本章覆盖一些数字和字符串对象、泛型、包等概念。

Numbers 和 Strings

数字和字符串(Number and String)介绍如何使用数字和字符串对象,还显示如何格式化数据输出。

Numbers

Numbers 类

使用数字时,大部分时间在代码中使用基本类型。 例如:

int i = 500;
float gpa = 3.65f;
byte mask = 0xff;

然而,有理由使用对象代替基本类型,Java平台为每个基本数据类型提供包装(wrapper)类。 这些类将“包装”基本类型到对象。 通常情况下,包装是由编译器完成的 - 如果您在需要使用对象的情况下使用基本类型,则编译器将为您包装基础类型为其包装类。 同样,如果在需要基本类型时使用数字对象,编译器将为您解开(unbox)对象。

所有数字包装类都是抽象类Number的子类:

Number

**注意:**还有另外四个Number的子类在这里没有讨论。 BigDecimal和BigInteger用于高精度计算。 AtomicInteger和AtomicLong用于多线程应用程序。

有三个原因可以使用Number对象而不是基本类型:

  • 作为一个期望对象的方法的参数(在处理数字集合时经常使用)。
  • 使用类定义的常量(如MIN_VALUE和MAX_VALUE),它们提供数据类型的上限和下限。
  • 使用类方法将值转换为其他基本类型以及从其他基本类型转换值,转换为字符串和从字符串转换,以及在数字系统(十进制,八进制,十六进制,二进制)之间进行转换。

下表列出了Number类的所有子类实现的实例方法。

Methods Implemented by all Subclasses of Number

Method Description
byte byteValue() short shortValue() int intValue() long longValue() float floatValue() double doubleValue() Converts the value of this Number object to the primitive data type returned.
int compareTo(Byte anotherByte) int compareTo(Double anotherDouble) int compareTo(Float anotherFloat) int compareTo(Integer anotherInteger) int compareTo(Long anotherLong) int compareTo(Short anotherShort) Compares this Number object to the argument.
boolean equals(Object obj) Determines whether this number object is equal to the argument. The methods return true if the argument is not null and is an object of the same type and with the same numeric value. There are some extra requirements for Double and Float objects that are described in the Java API documentation.

每个Number类都包含其他方法,这些方法可用于将数字转换为字符串和从字符串转换为数字以及在数字系统之间进行转换。 下表列出了Integer类中的这些方法。 其他Number子类的方法是相似的:

Conversion Methods, Integer Class

Method Description
static Integer decode(String s) Decodes a string into an integer. Can accept string representations of decimal, octal, or hexadecimal numbers as input.
static int parseInt(String s) Returns an integer (decimal only).
static int parseInt(String s, int radix) Returns an integer, given a string representation of decimal, binary, octal, or hexadecimal (radix equals 10, 2, 8, or 16 respectively) numbers as input.
String toString() Returns a String object representing the value of this Integer.
static String toString(int i) Returns a String object representing the specified integer.
static Integer valueOf(int i) Returns an Integer object holding the value of the specified primitive.
static Integer valueOf(String s) Returns an Integer object holding the value of the specified string representation.
static Integer valueOf(String s, int radix) Returns an Integer object holding the integer value of the specified string representation, parsed with the value of radix. For example, if s = “333” and radix = 8, the method returns the base-ten integer equivalent of the octal number 333.

格式化数字打印输出

printf 和 format 方法

java.io包中包含一个PrintStream类,它有两种格式化方法可用于替换print和println。 这些format和printf方法是相互等价的。您一直使用的熟悉的System.out恰好是一个PrintStream对象,因此您可以在System.out上调用PrintStream方法。 因此,你可以在你以前使用print或println的代码中的任何地方使用format或printf。 例如,

System.out.format(.....);

这两个java.io.PrintStream方法的语法是相同的:

public PrintStream format(String format, Object... args)

其中format是指定要使用的格式的字符串,args是使用该格式打印的变量的列表。 一个简单的例子是

System.out.format("The value of " + "the float variable is " +
     "%f, while the value of the " + "integer variable is %d, " +
     "and the string is %s", floatVar, intVar, stringVar);

第一个参数format是一个格式字符串,指定如何格式化第二个参数args中的对象。 格式字符串包含纯文本以及格式说明符(format specifiers),格式说明符是格式化Object … args参数的特殊字符。 (符号Object … args被称为varargs,这意味着参数的数量可能会有所不同。)

格式说明符以百分号(%)开头,并以转换器(converter)结尾。 转换器是一个字符,指示要格式化参数的类型。 在百分号(%)和转换器之间,可以有可选的标志和说明符。 java.util.Formatter中有许多转换器,标志和说明符

DecimalFormat 类

您可以使用java.text.DecimalFormat类来控制前导和尾随零,前缀和后缀的显示,分组(千位)分隔符和小数点分隔符。 DecimalFormat为数字格式提供了很大的灵活性,但它可以使你的代码更加复杂。

下面的示例通过将模式字符串传递给DecimalFormat构造函数来创建DecimalFormat对象myFormatter。 DecimalFormat从NumberFormat继承的format()方法然后由myFormatter调用 - 它接受一个double值作为参数,并返回一个字符串中格式化的数字:

下面是一个示例程序,演示DecimalFormat的使用:

import java.text.*;

public class DecimalFormatDemo {

   static public void customFormat(String pattern, double value ) {
      DecimalFormat myFormatter = new DecimalFormat(pattern);
      String output = myFormatter.format(value);
      System.out.println(value + "  " + pattern + "  " + output);
   }

   static public void main(String[] args) {

      customFormat("###,###.###", 123456.789);
      customFormat("###.##", 123456.789);
      customFormat("000000.000", 123.78);
      customFormat("$###,###.###", 12345.67);  
   }
}

输出是:

123456.789  ###,###.###  123,456.789
123456.789  ###.##  123456.79
123.78  000000.000  000123.780
12345.67  $###,###.###  $12,345.67

DecimalFormat.java Output

Value Pattern Output Explanation
123456.789 ###,###.### 123,456.789 The pound sign (#) denotes a digit, the comma is a placeholder for the grouping separator, and the period is a placeholder for the decimal separator.
123456.789 ###.## 123456.79 The value has three digits to the right of the decimal point, but the pattern has only two. The format method handles this by rounding up.
123.78 000000.000 000123.780 The pattern specifies leading and trailing zeros, because the 0 character is used instead of the pound sign (#).
12345.67 $###,###.### $12,345.67 The first character in the pattern is the dollar sign ($). Note that it immediately precedes the leftmost digit in the formatted output.

超越基础算术(Beyond Basic Arithmetic)

Java编程语言通过其算术运算符:+, - ,*,/和%来支持基本算术。 java.lang包中的Math类提供了进行更高级数学计算的方法和常量。

Math类中的方法都是静态的,所以可以直接从类中调用它们,如下所示:

Math.cos(angle);

注意: 使用static import语言功能,您不必在每个数学函数前写Math:

import static java.lang.Math.*;

这使您可以通过简单的名称来调用Math类的方法。 例如:

cos(angle);

常量和基本方法

Math类包含两个常量:

  • Math.E,这是自然对数(natural logarithms)的基础,和
  • Math.PI,它是一个圆的周长与其直径的比值( the ratio of the circumference of a circle to its diameter)。

Math类还包含40多个静态方法。 下表列出了一些基本的方法。

Basic Math Methods

Method Description
double abs(double d) float abs(float f) int abs(int i) long abs(long lng) Returns the absolute value of the argument.
double ceil(double d) Returns the smallest integer that is greater than or equal to the argument. Returned as a double.
double floor(double d) Returns the largest integer that is less than or equal to the argument. Returned as a double.
double rint(double d) Returns the integer that is closest in value to the argument. Returned as a double.
long round(double d) int round(float f) Returns the closest long or int, as indicated by the method’s return type, to the argument.
double min(double arg1, double arg2) float min(float arg1, float arg2) int min(int arg1, int arg2) long min(long arg1, long arg2) Returns the smaller of the two arguments.
double max(double arg1, double arg2) float max(float arg1, float arg2) int max(int arg1, int arg2) long max(long arg1, long arg2) Returns the larger of the two arguments.

指数和对数方法

下表列出了Math类的指数和对数方法。

Exponential and Logarithmic Methods

Method | Description double exp(double d) | Returns the base of the natural logarithms, e, to the power of the argument. double log(double d) | Returns the natural logarithm of the argument. double pow(double base, double exponent) | Returns the value of the first argument raised to the power of the second argument. double sqrt(double d) | Returns the square root of the argument.

三角法

Math类还提供了三角函数的集合,在下表中进行了总结。 传递给每个方法的值是用弧度表示的角度。 您可以使用toRadians方法将度数转换为弧度。

Trigonometric Methods

Method Description
double sin(double d) Returns the sine of the specified double value.
double cos(double d) Returns the cosine of the specified double value.
double tan(double d) Returns the tangent of the specified double value.
double asin(double d) Returns the arcsine of the specified double value.
double acos(double d) Returns the arccosine of the specified double value.
double atan(double d) Returns the arctangent of the specified double value.
double atan2(double y, double x) Converts rectangular coordinates (x, y) to polar coordinate (r, theta) and returns theta.
double toDegrees(double d) double toRadians(double d) Converts the argument to degrees or radians.

随机数字

random()方法返回0.0到1.0之间的伪随机选择的数字(pseudo-randomly selected number)。 范围包括0.0但不包括1.0。 换句话说:0.0 <= Math.random() < 1.0。 要获得不同范围的数字,可以对随机方法返回的值执行算术运算。 例如,要生成一个0到9之间的整数,你可以这样写:

int number = (int)(Math.random() * 10);

通过乘以10,可能值的范围变为0.0 <= number < 10.0。

当需要生成一个随机数时,使用Math.random可以很好地工作。 如果您需要生成一系列随机数字,您应该创建一个java.util.Random的实例并调用该对象上的方法来生成数字。

字符(Characters)

大多数情况下,如果使用单个字符值,则将使用基本字符(char)类型。 例如:

char ch = 'a'; 
// Unicode for uppercase Greek omega character
char uniChar = '\u03A9';
// an array of chars
char[] charArray = { 'a', 'b', 'c', 'd', 'e' };

然而,有时需要使用char作为对象,例如,将对象作为方法参数。 Java编程语言提供了一个包装类,用于为此目的“包装”char为Character对象。 Character类型的对象包含单个字段,其类型为char。 这个Character类还提供了许多有用的类(如静态)方法来处理字符。

您可以使用Character构造函数创建一个Character对象:

Character ch = new Character('a');

在某些情况下,Java编译器也会为您创建一个Character对象。 例如,如果将一个原始字符传递给需要对象的方法,编译器会自动将char转换为Character。 如果转换是以其他方式进行,则此功能称为自动装箱(autoboxing)或取消装箱(unboxing)。

**注意:**Character类是不可变的,所以一旦创建了,Character对象就不能被改变。

下表列出了Character类中一些最有用的方法,但并不详尽。 有关此类中所有方法(超过50个)的完整列表,请参阅java.lang.CharacterAPI规范。

Useful Methods in the Character Class

Method | Description boolean isLetter(char ch) boolean isDigit(char ch) | Determines whether the specified char value is a letter or a digit, respectively. boolean isWhitespace(char ch) | Determines whether the specified char value is white space. boolean isUpperCase(char ch) boolean isLowerCase(char ch) | Determines whether the specified char value is uppercase or lowercase, respectively. char toUpperCase(char ch) char toLowerCase(char ch) | Returns the uppercase or lowercase form of the specified char value. toString(char ch) | Returns a String object representing the specified character value — that is, a one-character string.

转义序列

以反斜杠(backslash, \)开头的字符是转义序列(escape sequence),对编译器有特殊意义。 下表显示了Java转义序列:

Escape Sequences

Escape Sequence Description
\t Insert a tab in the text at this point.
\b Insert a backspace in the text at this point.
\n Insert a newline in the text at this point.
\r Insert a carriage return in the text at this point.
\f Insert a formfeed in the text at this point.
' Insert a single quote character in the text at this point.
" Insert a double quote character in the text at this point.
\ Insert a backslash character in the text at this point.

当在打印语句中遇到转义序列时,编译器会相应地解释它。

字符串(Strings)

在Java编程中广泛使用的字符串(Strings)是一系列字符。 在Java编程语言中,字符串是对象。

Java平台提供了String类来创建和操作字符串。

创建字符串

创建字符串最直接的方法是编写:

String greeting = "Hello world!";

在这种情况下,“Hello world!” 是一个字符串字面值(string literal) - 用双引号括起来的代码中的一系列字符。 无论何时在代码中遇到字符串文字,编译器都会创建一个带有值的String对象 - 在这种情况下,Hello world!.

和任何其他对象一样,你可以使用new关键字和一个构造函数来创建String对象。 String类有十三个构造函数,它们允许您使用不同的来源(如字符数组)提供字符串的初始值:

char[] helloArray = { 'h', 'e', 'l', 'l', 'o', '.' };
String helloString = new String(helloArray);
System.out.println(helloString);

这段代码的最后一行显示hello。

**注意:**String类是不可变的,所以一旦创建了String对象就不能改变。 String类有许多方法,它们似乎是修改字符串的。 由于字符串是不可变的,这些方法真正做的是创建并返回一个包含操作结果的新字符串。

字符串长度

用于获取关于对象的信息的方法被称为访问器方法(accessor methods)。 一个可以用于字符串的存取方法(accessor method)是length()方法,它返回字符串对象中包含的字符数。 在执行了以下两行代码之后,len等于17:

String palindrome = "Dot saw I was Tod";
int len = palindrome.length();

回文(palindrome)是一个对称(symmetric)的词或句子,它前后拼写相同,忽略大小写和标点符号。 这是一个简短而低效的程序来翻转回文串。 它调用String方法charAt(i),它返回字符串中的第i个字符,从0开始计数。

public class StringDemo {
    public static void main(String[] args) {
        String palindrome = "Dot saw I was Tod";
        int len = palindrome.length();
        char[] tempCharArray = new char[len];
        char[] charArray = new char[len];
        
        // put original string in an 
        // array of chars
        for (int i = 0; i < len; i++) {
            tempCharArray[i] = 
                palindrome.charAt(i);
        } 
        
        // reverse array of chars
        for (int j = 0; j < len; j++) {
            charArray[j] =
                tempCharArray[len - 1 - j];
        }
        
        String reversePalindrome =
            new String(charArray);
        System.out.println(reversePalindrome);
    }
}

为了完成字符串反转,程序必须将字符串转换为字符数组(第一个for循环),将数组转换为第二个数组(第二个循环),然后转换回字符串。 String类包含一个方法getChars(),用于将字符串或字符串的一部分转换为字符数组,以便我们可以用上面的程序替换第一个for循环

palindrome.getChars(0, len, tempCharArray, 0);

连接字符串

String类包含一个连接(concatenating)两个字符串的方法:

string1.concat(string2);

这将返回一个新的string1字符串,并在末尾添加string2。

您也可以使用字符串字面量的concat()方法,如下所示:

"My name is ".concat("Rumplestiltskin");

字符串更常用+运算符连接,如

"Hello," + " world" + "!"

这导致了

"Hello, world!"

+运算符广泛用于打印语句。 例如:

String string1 = "saw I was ";
System.out.println("Dot " + string1 + "Tod");

将打印

Dot saw I was Tod

这样的连接(concatenation)可以是任何对象的混合。 对于每个不是String的对象,都会调用toString()方法将其转换为String。

**注意:**Java编程语言不允许字面量字符串跨越源文件中的行,所以您必须在多行字符串的每行末尾使用+连接运算符。 例如:

String quote = 
    "Now is the time for all good " +
    "men to come to the aid of their country.";

在print语句中,使用+连接运算符在行之间再次打断字符串是非常普遍的。

创建格式字符串

您已经看到使用printf()和format()方法来打印格式化数字的输出。 String类有一个等效的类方法format(),它返回一个String对象而不是一个PrintStream对象。

使用String的静态format()方法可以创建一个可重用的格式化的字符串,而不是一次性的打印语句。 例如,而不是

System.out.printf("The value of the float " +
                  "variable is %f, while " +
                  "the value of the " + 
                  "integer variable is %d, " +
                  "and the string is %s", 
                  floatVar, intVar, stringVar); 

你可以编写

String fs;
fs = String.format("The value of the float " +
                   "variable is %f, while " +
                   "the value of the " + 
                   "integer variable is %d, " +
                   " and the string is %s",
                   floatVar, intVar, stringVar);
System.out.println(fs);

转换数字和字符串

将字符串转换为数字

一个程序经常以字符串对象中的数字数据结束 - 例如用户输入的值。

包含基本数字类型(Byte,Integer,Double,Float,Long和Short)的Number子类每个都提供一个名为valueOf的类方法,将字符串转换为该类型的对象。 这里是一个例子,ValueOfDemo,从命令行获取两个字符串,将它们转换为数字,并对这些值执行算术运算:

public class ValueOfDemo {
    public static void main(String[] args) {

        // this program requires two 
        // arguments on the command line 
        if (args.length == 2) {
            // convert strings to numbers
            float a = (Float.valueOf(args[0])).floatValue(); 
            float b = (Float.valueOf(args[1])).floatValue();

            // do some arithmetic
            System.out.println("a + b = " +
                               (a + b));
            System.out.println("a - b = " +
                               (a - b));
            System.out.println("a * b = " +
                               (a * b));
            System.out.println("a / b = " +
                               (a / b));
            System.out.println("a % b = " +
                               (a % b));
        } else {
            System.out.println("This program " +
                "requires two command-line arguments.");
        }
    }
}

以下是使用4.5和87.2作为命令行参数时程序的输出:

a + b = 91.7
a - b = -82.7
a * b = 392.4
a / b = 0.0516055
a % b = 4.5

**注意:**每个包含基本数字类型的Number子类也提供了一个parseXXXX()方法(例如,parseFloat()),可用于将字符串转换为基本数字。 由于返回的是基本类型而不是对象,因此parseFloat()方法比valueOf()方法更直接。 例如,在ValueOfDemo程序中,我们可以使用:

float a = Float.parseFloat(args[0]);
float b = Float.parseFloat(args[1]);

将数字转换为字符串

有时您需要将数字转换为字符串,因为您需要对其字符串形式的值进行操作。 有几个简单的方法可以将数字转换为字符串:

int i;
// Concatenate "i" with an empty string; conversion is handled for you.
String s1 = "" + i;

// The valueOf class method.
String s2 = String.valueOf(i);

每个Number子类都包含一个类方法toString(),它将其基本类型转换为字符串。 例如:

int i;
double d;
String s3 = Integer.toString(i); 
String s4 = Double.toString(d);

操纵字符串中的字符

String类有许多方法用于检查字符串的内容,查找字符串中的字符或子字符串,更改大小写以及其他任务。

通过索引获取字符和子字符串

您可以通过调用charAt()存取方法来获取字符串中特定索引处的字符。 第一个字符的索引是0,而最后一个字符的索引是length() - 1。 例如,下面的代码在字符串中获取索引9处的字符:

String anotherPalindrome = "Niagara. O roar again!"; 
char aChar = anotherPalindrome.charAt(9);

如果要从字符串中获取多个连续的字符,可以使用substring方法。 子串方法有两个版本,如下表所示:

The substring Methods in the String Class

Method Description
String substring(int beginIndex, int endIndex) Returns a new string that is a substring of this string. The substring begins at the specified beginIndex and extends to the character at index endIndex - 1.
String substring(int beginIndex) Returns a new string that is a substring of this string. The integer argument specifies the index of the first character. Here, the returned substring extends to the end of the original string.

其他操纵字符串的方法

Other Methods in the String Class for Manipulating Strings

Method | Description String[] split(String regex) String[] split(String regex, int limit) | Searches for a match as specified by the string argument (which contains a regular expression) and splits this string into an array of strings accordingly. The optional integer argument specifies the maximum size of the returned array. Regular expressions are covered in the lesson titled “Regular Expressions.” CharSequence subSequence(int beginIndex, int endIndex) | Returns a new character sequence constructed from beginIndex index up until endIndex - 1. String trim() | Returns a copy of this string with leading and trailing white space removed. String toLowerCase() String toUpperCase() | Returns a copy of this string converted to lowercase or uppercase. If no conversions are necessary, these methods return the original string.

在字符串中搜索字符和子字符串

这里有一些其他的String方法,用于查找字符串中的字符或子字符串。 String类提供访问器方法,它返回特定字符或子字符串的位置:indexOf()和lastIndexOf()。 indexOf()方法从字符串的开头向前搜索,lastIndexOf()方法从字符串的末尾向后搜索。 如果找不到字符或子字符串,indexOf()和lastIndexOf()返回-1。

String类还提供了一个搜索方法contains,如果该字符串包含特定的字符序列,则返回true。 当你只需要知道字符串包含一个字符序列时,使用这个方法,但是精确的位置并不重要。

The Search Methods in the String Class

Method Description
int indexOf(int ch) int lastIndexOf(int ch) Returns the index of the first (last) occurrence of the specified character.
int indexOf(int ch, int fromIndex) int lastIndexOf(int ch, int fromIndex) Returns the index of the first (last) occurrence of the specified character, searching forward (backward) from the specified index.
int indexOf(String str) int lastIndexOf(String str) Returns the index of the first (last) occurrence of the specified substring.
int indexOf(String str, int fromIndex) int lastIndexOf(String str, int fromIndex) Returns the index of the first (last) occurrence of the specified substring, searching forward (backward) from the specified index.
boolean contains(CharSequence s) Returns true if the string contains the specified character sequence.

**注意:**CharSequence是一个由String类实现的接口。 因此,可以使用字符串作为contains()方法的参数。

将字符和子字符替换为字符串

String类有很少的方法可以将字符或子字符串插入到字符串中。 一般情况下,它们不是必需的:您可以通过将您从字符串中删除的子字符串与要插入的子字符串连接来创建新字符串。

但是,String类有四个方法来替换找到的字符或子字符串。 他们是:

Methods in the String Class for Manipulating Strings

Method Description
String replace(char oldChar, char newChar) Returns a new string resulting from replacing all occurrences of oldChar in this string with newChar.
String replace(CharSequence target, CharSequence replacement) Replaces each substring of this string that matches the literal target sequence with the specified literal replacement sequence.
String replaceAll(String regex, String replacement) Replaces each substring of this string that matches the given regular expression with the given replacement.
String replaceFirst(String regex, String replacement) Replaces the first substring of this string that matches the given regular expression with the given replacement.

比较字符串和部分字符串

String类有许多用于比较字符串和字符串部分(Portions)的方法。 下表列出了这些方法。

Methods for Comparing Strings

Method Description
boolean endsWith(String suffix) boolean startsWith(String prefix) Returns true if this string ends with or begins with the substring specified as an argument to the method.
boolean startsWith(String prefix, int offset) Considers the string beginning at the index offset, and returns true if it begins with the substring specified as an argument.
int compareTo(String anotherString) Compares two strings lexicographically. Returns an integer indicating whether this string is greater than (result is > 0), equal to (result is = 0), or less than (result is < 0) the argument.
int compareToIgnoreCase(String str) Compares two strings lexicographically, ignoring differences in case. Returns an integer indicating whether this string is greater than (result is > 0), equal to (result is = 0), or less than (result is < 0) the argument.
boolean equals(Object anObject) Returns true if and only if the argument is a String object that represents the same sequence of characters as this object.
boolean equalsIgnoreCase(String anotherString) Returns true if and only if the argument is a String object that represents the same sequence of characters as this object, ignoring differences in case.
boolean regionMatches(int toffset, String other, int ooffset, int len) Tests whether the specified region of this string matches the specified region of the String argument. Region is of length len and begins at the index toffset for this string and ooffset for the other string.
boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) Tests whether the specified region of this string matches the specified region of the String argument. Region is of length len and begins at the index toffset for this string and ooffset for the other string. The boolean argument indicates whether case should be ignored; if true, case is ignored when comparing characters.
boolean matches(String regex) Tests whether this string matches the specified regular expression. Regular expressions are discussed in the lesson titled “Regular Expressions.”

StringBuilder类

StringBuilder对象就像String对象,除了它们可以被修改。在内部,这些对象被视为包含一系列字符的变长数组。在任何时候,序列的长度和内容都可以通过方法调用来改变。

应该始终使用Strings,除非字符串构建器在简单代码或更好的性能方面提供了优势。例如,如果您需要连接大量的字符串,则追加到StringBuilder对象会更高效。

长度和容量

StringBuilder类与String类一样,有一个length()方法,它返回构建器中字符序列的长度。

与字符串不同,每个字符串生成器也有一个容量(capacity),即已分配的字符空间的数量。 capacity()方法返回的容量总是大于或等于length(通常大于),并且会根据需要自动扩展以适应字符串构建器的添加。

StringBuilder Constructors

Constructor Description
StringBuilder() Creates an empty string builder with a capacity of 16 (16 empty elements).
StringBuilder(CharSequence cs) Constructs a string builder containing the same characters as the specified CharSequence, plus an extra 16 empty elements trailing the CharSequence.
StringBuilder(int initCapacity) Creates an empty string builder with the specified initial capacity.
StringBuilder(String s) Creates a string builder whose value is initialized by the specified string, plus an extra 16 empty elements trailing the string.

StringBuilder类有一些与String类没有的长度和容量相关的方法:

Length and Capacity Methods

Method Description
void setLength(int newLength) Sets the length of the character sequence. If newLength is less than length(), the last characters in the character sequence are truncated. If newLength is greater than length(), null characters are added at the end of the character sequence.
void ensureCapacity(int minCapacity) Ensures that the capacity is at least equal to the specified minimum.

一些操作(例如,append(),insert()或setLength())可以增加字符串生成器中字符序列的长度,使得生成的length()将大于当前的capacity()。 发生这种情况时,容量会自动增加。

StringBuilder操作

StringBuilder在String中没有的主要操作是append()和insert()方法,它们被重载以接受任何类型的数据。 每个将其参数转换为一个字符串,然后将该字符串的字符追加或插入到字符串构建器中的字符序列中。 append方法总是在现有字符序列的末尾添加这些字符,而insert方法则在指定的点添加字符。

这里有一些StringBuilder类的方法。

Various StringBuilder Methods

Method Description
StringBuilder append(boolean b) StringBuilder append(char c) StringBuilder append(char[] str) StringBuilder append(char[] str, int offset, int len) StringBuilder append(double d) StringBuilder append(float f) StringBuilder append(int i) StringBuilder append(long lng) StringBuilder append(Object obj) StringBuilder append(String s) Appends the argument to this string builder. The data is converted to a string before the append operation takes place.
StringBuilder delete(int start, int end) StringBuilder deleteCharAt(int index) The first method deletes the subsequence from start to end-1 (inclusive) in the StringBuilder’s char sequence. The second method deletes the character located at index.
StringBuilder insert(int offset, boolean b) StringBuilder insert(int offset, char c) StringBuilder insert(int offset, char[] str) StringBuilder insert(int index, char[] str, int offset, int len) StringBuilder insert(int offset, double d) StringBuilder insert(int offset, float f) StringBuilder insert(int offset, int i) StringBuilder insert(int offset, long lng) StringBuilder insert(int offset, Object obj) StringBuilder insert(int offset, String s) Inserts the second argument into the string builder. The first integer argument indicates the index before which the data is to be inserted. The data is converted to a string before the insert operation takes place.
StringBuilder replace(int start, int end, String s) void setCharAt(int index, char c) Replaces the specified character(s) in this string builder.
StringBuilder reverse() Reverses the sequence of characters in this string builder.
String toString() Returns a string that contains the character sequence in the builder.

**注意:**通过首先使用StringBuilder类的toString()方法将字符串构建器转换为字符串,可以在StringBuilder对象上使用任何String方法。 然后使用StringBuilder(String str)构造函数将字符串转换回字符串生成器。

Autoboxing 和 Unboxing

自动装箱(Autoboxing)是Java编译器在基本类型和相应的对象包装类之间进行的自动转换。 例如,将int转换为Integer,double转换为Double等等。 如果转换是另一种方式,则称为拆箱(unboxing)。

这是自动装箱最简单的例子:

Character ch = 'a';

本节中的其他示例使用泛型。

考虑下面的代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(i);

虽然您将int值作为基本类型(而不是Interger对象)添加到li,但代码将进行编译。 因为li是Integer对象列表,而不是int值列表,所以您可能会想知道为什么Java编译器不会发出编译时错误。 编译器不会生成错误,因为它会从i创建一个Integer对象,并将该对象添加到li中。 因此,编译器在运行时将以前的代码转换为以下代码:

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
    li.add(Integer.valueOf(i));

将基本值(例如int)转换为相应包装类(Integer)的对象称为自动装箱。 Java编译器应用自动装箱(autoboxing)到基本值是:

  • 作为参数传递给期望相应包装类的对象的方法。
  • 分配给相应的包装类的变量。

考虑以下方法:

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i: li)
        if (i % 2 == 0)
            sum += i;
        return sum;
}

由于余数(%)和一元加号(+=)运算符不适用于Integer对象,因此您可能想知道为什么Java编译器编译方法时不会发出任何错误。 编译器不会生成错误,因为它在运行时调用intValue方法将Integer转换为int:

public static int sumEven(List<Integer> li) {
    int sum = 0;
    for (Integer i : li)
        if (i.intValue() % 2 == 0)
            sum += i.intValue();
        return sum;
}

将包装类型(Integer)的对象转换为其对应的基本(int)值称为拆箱(unboxing)。 Java编译器应用拆箱到包装类的对象是:

  • 作为参数传递给期望相应基本类型值的方法。
  • 分配给相应基本类型的变量。

Unboxing示例显示了这是如何工作的:

import java.util.ArrayList;
import java.util.List;

public class Unboxing {

    public static void main(String[] args) {
        Integer i = new Integer(-8);

        // 1. Unboxing through method invocation
        int absVal = absoluteValue(i);
        System.out.println("absolute value of " + i + " = " + absVal);

        List<Double> ld = new ArrayList<>();
        ld.add(3.1416);    // Π is autoboxed through method invocation.

        // 2. Unboxing through assignment
        double pi = ld.get(0);
        System.out.println("pi = " + pi);
    }

    public static int absoluteValue(int i) {
        return (i < 0) ? -i : i;
    }
}

Autoboxing和Unboxing可以让开发人员编写更清晰的代码,使其更易于阅读。 下表列出了Java编译器用于Autoboxing和Unboxing的基本类型及其对应的包装类:

Primitive type Wrapper class
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double

泛型

泛型(Generic)是 Java 编程语言的强大功能。 它们可以改善代码的类型安全性,从而在编译时更多地检测到错误。

在任何复杂(nontrivial)的软件项目中,bug都只是生活中的事实。 仔细的计划,编程和测试可以帮助减少他们的普遍性,但不知何故,在某个地方,他们总是会找到一种方法来进入你的代码。 随着新功能的推出以及您的代码库规模和复杂性的增加,这一点变得尤为明显。

幸运的是,一些bug比其他bug更容易被发现。 例如,编译时错误可以在早期发现; 你可以使用编译器的错误信息来找出问题所在,然后解决问题。 然而,运行时错误可能是更多的问题; 它们并不总是立即出现,而且当它们这样做时,它可能在程序中的某一点远离问题的实际原因。

泛型通过在编译时检测更多的错误来增加代码的稳定性。

为什么使用泛型?

简而言之(In a nutshell),泛型使类型(类和接口)在定义类,接口和方法时成为参数。就像方法声明中使用的更熟悉的形式参数一样,类型参数为您提供了一种方法,让您在不同的输入中重用相同的代码。区别在于形式参数的输入是值,而类型参数的输入是类型。

使用泛型的代码比非泛型代码有许多优点:

  • 编译时更强大的类型检查。

Java编译器将强类型检查应用于泛型代码,并在代码违反类型安全性时发出错误。修复编译时错误比修复运行时错误要容易得多。

  • 消除强制转换。

以下没有泛型的代码段需要强制转换:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);

当重写为使用泛型时,代码不需要强制转换:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast
  • 使程序员能够实现泛型算法。

通过使用泛型,程序员可以实现泛型算法,这些泛型算法可以处理不同类型的集合,可以进行定制,并且类型安全,易于阅读。

泛型类型

泛型类型(generic type)是通过类型参数化的泛型类或接口。 下面的Box类将被修改来演示这个概念。

一个简单的Box类

首先检查在任何类型的对象上运行的非泛型Box类。 它只需要提供两种方法:set,将对象添加到box,get,获取它:

public class Box {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

由于它的方法接受或返回一个Object,你可以自由地传入任何你想要的,只要它不是基本类型之一。 没有办法在编译时验证类是如何使用的。 代码的一部分可能会放置一个Integer到box,并期望将Integer从中取出,而另一部分代码可能会错误地传递一个String,导致运行时错误。

泛型版的Box类

泛型类用以下格式定义:

class name<T1, T2, ..., Tn> { /* ... */ }

由尖括号(<>)分隔的类型参数部分在类名后面。 它指定了类型参数(type parameters, 也称为类型变量 - type variables)T1,T2,…,和Tn。

要更新Box类以使用泛型,可以通过将代码“public class Box”更改为“public class Box”来创建泛型类型声明。 这引入了类型变量T,可以在类中的任何地方使用。

随着这个变化,Box类变成:

/**
 * Generic version of the Box class.
 * @param <T> the type of the value being boxed
 */
public class Box<T> {
    // T stands for "Type"
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

正如你所看到的,所有出现的Object被T替换。一个类型变量可以是你指定的任何非基本类型:任何类(class)类型,任何接口类型,任何数组类型,甚至是另一个类型变量。

这种相同的技术可以应用于创建泛型接口。

输入参数命名约定

按照惯例,类型参数名称是单个大写字母。 这与你已经知道的变量命名约定形成了鲜明的对比,并且有很好的理由:没有这个约定,很难区分类型变量和普通类或接口名称。

最常用的类型参数名称是:

  • E - Element,元素(广泛用于Java集合框架)
  • K - Key,键
  • N - Number,数字
  • T - Type,类型
  • V - Value,值
  • S,U,V等 - 2nd,3rd,4th 类型

调用和实例化一个泛型类型

要从代码中引用泛型Box类,必须执行泛型类型调用(generic type invocation),它用一些具体的值替换T,例如Integer:

Box<Integer> integerBox;

您可以将泛型类型调用看作与普通的方法调用类似,但不是将一个参数传递给方法,而是将类型参数(type argument, 本例中为Integer)传递给Box类本身。

**Type Parameter and Type Argument Terminology(术语):**许多开发者交替使用术语“type parameter”和“type argument”,但是这些术语是不一样的。编码时,为了创建参数化类型,提供了类型参数(type arguments)。因此,Foo<T>中的T是一个type parameter,而Foo<String> f中的String是一个type argument。

像任何其他变量声明一样,这段代码实际上并不创建一个新的Box对象。它只是声明integerBox将持有一个“Interger Box”的引用,以及如何读取Box

泛型类型的调用通常称为参数化类型(parameterized type)。

为了实例化这个类,像往常一样使用new关键字,但在类名和括号之间放置

Box<Integer> integerBox = new Box<Integer>();

The Diamond

在Java SE 7和更高版本中,只要编译器可以根据上下文确定或推断类型参数,就可以用一组空类型参数(<>)替换调用泛型类的构造函数所需的类型参数。 这一对尖括号,<>,非正式地称为diamond。 例如,您可以使用以下语句创建Box的实例:

Box<Integer> integerBox = new Box<>();

多个类型参数

如前所述,泛型类可以有多个类型参数。 例如,泛型OrderedPair类,它实现了泛型Pair接口:

public interface Pair<K, V> {
    public K getKey();
    public V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {

    private K key;
    private V value;

    public OrderedPair(K key, V value) {
	this.key = key;
	this.value = value;
    }

    public K getKey()	{ return key; }
    public V getValue() { return value; }
}

以下语句创建OrderedPair类的两个实例:

Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String>  p2 = new OrderedPair<String, String>("hello", "world");

代码new OrderedPair<String, Integer>实例化K作为String,V作为Interger。 因此,OrderedPair的构造函数的参数类型分别是String和Integer。 由于autoboxing,将String和int传递给类是有效的。

正如 diamond 中所提到的,因为Java编译器可以从OrderedPair<String,Integer>声明中推断K和V类型,所以可以使用菱形符号(diamond notation)缩短(shortened)这些语句:

OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String>  p2 = new OrderedPair<>("hello", "world");

要创建泛型接口,请遵循与创建泛型类相同的约定。

参数化类型

也可以用参数化类型(即List)替换类型参数(即K或V)。 例如,使用OrderedPair<K, V>示例:

OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));

原始类型

原始类型(raw type)是没有任何类型参数的泛型类或接口的名称。 例如,给定通用Box类:

public class Box<T> {
    public void set(T t) { /* ... */ }
    // ...
}

要创建Box的参数化类型,可以为形式类型参数T提供实际的类型参数:

Box<Integer> intBox = new Box<>();

如果省略实际的类型参数,则创建一个原始类型的Box

Box rawBox = new Box();

因此,Box是泛型Box的原始类型。 但是,非泛型类或接口类型不是原始类型。

原始类型显示在传统代码中,因为很多API类(如Collections类)在JDK 5.0之前不是泛型。 使用原始类型时,您基本上会获得预泛型的行为(pre-generics behavior) - Box会为您提供Objects。 为了向后兼容,允许将参数化类型分配给其原始类型:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;               // OK

但是,如果将原始类型分配给参数化类型,则会收到警告:

Box rawBox = new Box();           // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox;     // warning: unchecked conversion

如果使用原始类型来调用在相应泛型中定义的泛型方法,也会得到警告:

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8);  // warning: unchecked invocation to set(T)

该警告显示原始类型绕过了泛型类型检查,将不安全代码的捕获推迟到运行时。 因此,你应该避免使用原始类型。

未经检查的错误消息

如前所述,将普通(legacy)代码与泛型代码混合在一起时,可能会遇到类似于以下内容的警告消息:

Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

在使用旧API操作原始类型时,可能会发生这种情况,如以下示例所示:

public class WarningDemo {
    public static void main(String[] args){
        Box<Integer> bi;
        bi = createBox();
    }

    static Box createBox(){
        return new Box();
    }
}

术语“unchecked”表示编译器没有足够的类型信息来执行确保类型安全所需的所有类型的检查。 虽然编译器提供了一个提示,但是默认情况下,“unchecked”警告被禁用。 要查看所有“unchecked”警告,请使用-Xlint:unchecked进行重新编译。

使用-Xlint:unchecked重新编译前面的示例会显示以下附加信息:

WarningDemo.java:4: warning: [unchecked] unchecked conversion
found   : Box
required: Box<java.lang.Integer>
        bi = createBox();
                      ^
1 warning

要完全禁用未经检查的警告,请使用-Xlint:-unchecked标志。 @SuppressWarnings(“unchecked”)注释禁止未经检查的警告。

泛型方法

泛型方法(Generic methods)是引入自己的类型参数的方法。 这与声明泛型类型相似,但是类型参数的作用域仅限于声明它的方法。 允许使用静态和非静态泛型方法,以及泛型类构造函数。

泛型方法的语法包括一个类型参数列表,里面的尖括号出现在方法的返回类型之前。 对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。

Util类包含一个泛型方法compare,用于比较两个Pair对象:

public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p1.getValue().equals(p2.getValue());
    }
}

public class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    public K getKey()   { return key; }
    public V getValue() { return value; }
}

调用此方法的完整语法是:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);

类型已经明确提供,如粗体所示。 通常,这可以省略,编译器会推断出所需的类型:

Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);

这个功能称为类型推断(type inference),它允许你像通常的方法那样调用泛型方法,而不用在尖括号之间指定类型。

有界类型参数

有时可能需要限制可以用作参数化类型的类型参数的类型。 例如,对数字进行操作的方法可能只想接受Number或其子类的实例。 这是有界的类型参数(bounded type parameters)。

要声明有界的类型参数,请列出类型参数的名称,其后跟随extends关键字,后跟其上限(upper bound),在本例中为Number。 请注意,在这种情况下,extends在一般意义上用来表示“扩展(extends)”(如在类中)或“实现(implements)”(如在接口中)。

public class Box<T> {

    private T t;          

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }

    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.set(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String!
    }
}

通过修改我们的泛型方法来包含这个有界的类型参数,编译将会失败,因为我们调用inspect仍然包含一个String:

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                  ^
1 error

除了限制可用于实例化泛型的类型之外,有界的类型参数还允许您调用在边界中定义的方法:

public class NaturalNumber<T extends Integer> {

    private T n;

    public NaturalNumber(T n)  { this.n = n; }

    public boolean isEven() {
        return n.intValue() % 2 == 0;
    }

    // ...
}

isEven方法通过n调用Integer类中定义的intValue方法。

多个边界

前面的例子说明了使用带有单个边界的类型参数,但是一个类型参数可以有多个边界:

<T extends B1 & B2 & B3>

具有多个边界的类型变量是边界中列出的所有类型的子类型。 如果其中一个边界是一个类,则必须先指定它。 例如:

Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

如果未先指定绑定A,则会出现编译时错误:

class D <T extends B & A & C> { /* ... */ }  // compile-time error

泛型方法和有界类型参数

有界的类型参数是实现泛型算法的关键。 考虑下面的方法来计算大于指定元素elem的数组T[]中的元素数量。

public static <T> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e > elem)  // compiler error
            ++count;
    return count;
}

该方法的实现很简单,但是它不能编译,因为大于运算符(>)仅适用于诸如short,int,double,long,float,byte和char等基本类型。 您不能使用>运算符来比较对象。 要解决此问题,请使用由Comparable接口定界的类型参数:

public interface Comparable<T> {
    public int compareTo(T o);
}

由此产生的代码将是:

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

泛型,继承和子类型

正如你所知道的,只要类型是兼容的,就可以将一种类型的对象分配给另一种类型的对象。 例如,您可以将一个Integer赋值给一个Object,因为Object是Integer的超类型之一:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;   // OK

在面向对象的术语中,这被称为“是(is a)”的关系。 由于Integer是一种Object,所以赋值是允许的。 但Interger也是一种Number,所以下面的代码也是有效的:

public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

这在泛型也是如此。 您可以执行泛型类型调用,将Number作为其类型参数传递,如果参数与Number兼容,则可以允许任何后续的add调用:

Box<Number> box = new Box<Number>();
box.add(new Integer(10));   // OK
box.add(new Double(10.1));  // OK

现在考虑下面的方法:

public void boxTest(Box<Number> n) { /* ... */ }

它接受什么类型的参数? 通过查看它的签名,可以看到它接受一个类型为Box的单个参数。 但是,这是什么意思? 是否允许按照您所期望的方式传入Box或Box? 答案是“否”,因为Box和Box不是Box的子类型。

在泛型编程中,这是一个常见的误解,但这是一个很重要的概念。

Box is not a subtype of Box even though Integer is a subtype of Number.

即使Integer是Number的子类型,Box 也不是Box 的子类型。

**注意:**给定两个具体类型A和B(例如Number和Integer),MyClass与MyClass没有关系,不管A和B是否相关。 MyClass和MyClass的公共父项是Object。

泛型类和子类型

您可以通过扩展或实现泛型类或接口来子类型。 一个类或接口的类型参数与另一个类的类型参数之间的关系由extends和implements子句决定。

以Collections类为例,ArrayList实现List,List扩展Collection。 所以ArrayList是List的一个子类型,它是Collection的一个子类型。 只要不改变类型参数,子类型关系在类型之间保留。

A sample Collections hierarchy

A sample Collections hierarchy

现在设想我们想要定义我们自己的列表接口PayloadList,它将泛型类型P的可选值与每个元素相关联。 其声明可能如下所示:

interface PayloadList<E,P> extends List<E> {
  void setPayload(int index, P val);
  ...
}

PayloadList的以下参数是List的子类型:

  • PayloadList<String,String>
  • PayloadList<String,Integer>
  • PayloadList<String,Exception>

A sample PayloadList hierarchy

A sample PayloadList hierarchy

类型推断

类型推断(Type inference)是Java编译器查看每个方法调用和相应声明以确定使调用适用的类型参数的能力。 推理算法确定参数的类型,如果可用,则确定结果被分配或返回的类型。 最后,推理算法试图找到最适合所有参数的特定类型。

为了说明最后一点,在下面的例子中,推断确定传递给pick方法的第二个参数的类型是Serializable:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

类型推断和泛型方法

泛型方法为你介绍了类型推断,它可以像普通方法(ordinary method)那样调用泛型方法,而不需要在尖括号之间指定类型。 考虑下面的例子BoxDemo,它需要Box类:

public class BoxDemo {

  public static <U> void addBox(U u, 
      java.util.List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
    int counter = 0;
    for (Box<U> box: boxes) {
      U boxContents = box.get();
      System.out.println("Box #" + counter + " contains [" +
             boxContents.toString() + "]");
      counter++;
    }
  }

  public static void main(String[] args) {
    java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
      new java.util.ArrayList<>();
    BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
    BoxDemo.outputBoxes(listOfIntegerBoxes);
  }
}

以下是这个例子的输出:

Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]

泛型方法addBox定义了一个名为U的类型参数。通常,Java编译器可以推断泛型方法调用的类型参数。 因此,在大多数情况下,您不必指定它们。 例如,要调用泛型方法addBox,可以使用类型见证(type witness)来指定类型参数,如下所示:

BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

或者,如果省略类型见证(type witness),则Java编译器会自动从(来自方法的参数)推断类型参数是Integer:

BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);

类型推断和泛型类实例化

只要编译器可以从上下文中推断出类型参数,就可以用一组空的类型参数(<>)替换调用泛型类的构造函数所需的类型参数。 这对尖括号非正式地称为diamond。

例如,考虑下面的变量声明:

Map<String, List<String>> myMap = new HashMap<String, List<String>>();

您可以使用一组空的类型参数(<>)替换构造函数的参数化类型:

Map<String, List<String>> myMap = new HashMap<>();

请注意,为了在泛型类实例化过程中利用类型推断,您必须使用diamond。 在以下示例中,编译器会生成未经检查的转换警告,因为HashMap()构造函数引用HashMap原始类型,而不是Map<String, List类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

类型推断和泛型和非泛型类的泛型构造函数

请注意,构造函数在泛型类和非泛型类中都可以是泛型的(换句话说,声明它们自己的正式类型参数)。 考虑下面的例子:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

考虑以下类MyClass的实例:

new MyClass<Integer>("")

此语句创建参数化类型MyClass的实例; 该语句明确指定泛型类MyClass的形式类型参数(formal type parameter)X的类型Integer。 请注意,此泛型类的构造函数包含一个形式类型参数T.编译器推断出此泛型类的构造函数的形式类型参数T的类型String(因为此构造函数的实际参数是一个String对象)。

Java SE 7之前版本的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。 但是,如果使用diamond(<>),Java SE 7及更高版本中的编译器可以推断实例化的泛型类的实际类型参数。 考虑下面的例子:

MyClass<Integer> myObject = new MyClass<>("");

在此示例中,编译器推断泛型类MyClass的形式类型参数X的类型为Interger. 它推断了这个泛型类的构造函数的形式类型参数T的类型String。

**注意:**重要的是要注意,推断算法只使用调用参数,目标类型以及可能的显式预期返回类型来推断类型。 推断算法不会使用程序中稍后的结果。

目标类型

Java编译器利用目标类型来推断泛型方法调用的类型参数。 表达式的目标类型是Java编译器期望取决于表达式出现位置的数据类型。 考虑一下Collections.emptyList方法,声明如下:

static <T> List<T> emptyList();

考虑下面的赋值语句:

List<String> listOne = Collections.emptyList();

这个语句需要一个List的实例; 这个数据类型是目标类型。 因为emptyList方法返回List类型的值,编译器推断类型参数T必须是String值。 这可以在Java SE 7和8中使用。或者,您可以使用类型见证(type witness)并按如下所示指定T的值:

List<String> listOne = Collections.<String>emptyList();

但是,在这方面这不是必要的。 尽管如此,在其他情况下也是必要的。 考虑以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假设你想用一个空的列表来调用processStringList方法。 在Java SE 7中,以下语句不能编译:

processStringList(Collections.emptyList());

Java SE 7编译器生成类似于以下内容的错误消息:

List<Object> cannot be converted to List<String>

编译器需要一个类型参数T的值,所以它以Object的值开始。 因此,Collections.emptyList的调用返回一个List类型的值,它与processStringList方法不兼容。 因此,在Java SE 7中,您必须按如下方式指定类型参数值的值:

processStringList(Collections.<String>emptyList());

这在Java SE 8中不再是必需的。什么是目标类型的概念已被扩展为包含方法参数,例如方法processStringList的参数。 在这种情况下,processStringList需要一个List类型的参数。 Collections.emptyList方法返回List的值,因此使用List的目标类型,编译器推断类型参数T的值为String。 因此,在Java SE 8中,编译了以下语句:

processStringList(Collections.emptyList());

通配符

在泛型代码中,被称为通配符(wildcard)的问号(?)表示未知类型。 通配符可用于多种情况:作为参数的类型,字段或局部变量; 有时作为返回类型(尽管更好的编程实践更具体)。 通配符永远不会用作泛型方法调用的类型参数,泛型类实例创建或超类型。

以下部分更详细地讨论通配符,包括上限通配符,下限通配符和通配符捕获。

上限通配符

您可以使用上限通配符(upper bounded wildcard)来放宽对变量的限制。 例如,假设你想写一个在List,List和List上工作的方法; 你可以通过使用上限通配符来实现这一点。

要声明上限通配符,请使用通配符('?'),接着是extends关键字,后跟其上边界(upper bound)。 请注意,在这种情况下,extends在一般意义上用来表示“extends”(如在类中)或“implements”(如在接口中)。

要编写在Number和Number的子类型(例如Integer,Double和Float)列表上工作的方法,可以指定List<? extends Number>。 术语List比List<?extends Number>更具限制性,因为前者只匹配类型Number的列表,而后者匹配Number或其任何子类的列表。

考虑以下process方法:

public static void process(List<? extends Foo> list) { /* ... */ }

上限通配符<? extends Foo>,其中Foo是任何类型,匹配Foo和Foo的任何子类型。process方法可以访问列表元素作为类型Foo:

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

在foreach子句中,elem变量迭代列表中的每个元素。 任何在Foo类中定义的方法现在都可以在elem上使用。

sumOfList方法返回列表中数字的总和:

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

以下代码使用Integer对象列表打印sum = 6.0:

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

Double值的列表可以使用相同的sumOfList方法。 以下代码打印sum = 7.0:

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

无界的通配符

无界通配符类型(unbounded wildcard type)使用通配符(?)指定,例如List<?>。 这被称为未知类型的列表(list of unknown type)。 有两种情况,一个无界通配符是一个有用的方法:

  • 如果您正在编写一个可以使用Object类中提供的功能实现的方法。
  • 当代码使用泛型类中不依赖于类型参数的方法时。 例如,List.size或List.clear。 事实上,Class<?>经常被使用,因为Class中的大多数方法都不依赖于T.

考虑下面的方法printList:

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

printList的目标是打印任何类型的列表,但是它无法实现这个目标 - 它仅打印一个Object实例列表; 它不能打印List,List,List等,因为它们不是List的子类型。 要写一个通用的printList方法,使用List<?>:

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

因为对于任何具体类型A,List是List<?>的子类型,所以可以使用printList打印任何类型的列表:

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);

**注意:**整个课程中的示例都使用了Arrays.asList方法。 这个静态工厂方法转换指定的数组并返回一个固定大小的列表。

注意List和List中。

下限通配符

下限通配符将未知类型限制为该类型的特定类型或超类型。

使用通配符('?')表示较低的有界通配符,紧随其后的是super关键字,紧跟其下界:<? super A>。

**注意:**您可以为通配符指定上限,也可以指定下限,但不能同时指定两者。

假设你想写一个把Integer对象放入列表的方法。为了最大限度地提高灵活性,您希望方法在List,List和List上工作 - 任何可以保存Integer值的东西。

要编写在Integer列表和Integer的超类型(例如Integer,Number和Object)上工作的方法,您可以指定List<? super Interger>。术语List比List<? super Interger>更具限制性,因为前者只匹配Integer类型的列表,而后者匹配任何Integer类型的任何类型的列表。

以下代码将数字1到10添加到列表的末尾:

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

通配符和子类型

如泛型,继承和子类型中所述,泛型类或接口不仅仅是因为它们的类型之间有关系而相关的。 但是,可以使用通配符在泛型类或接口之间创建关系。

给定以下两个常规(非泛型)类:

class A { /* ... */ }
class B extends A { /* ... */ }

写下面的代码是合理的:

B b = new B();
A a = b;

这个例子显示了常规类的继承遵循这种子类型的规则:如果B扩展A,则类B是类A的子类型。该规则不适用于泛型类型:

List<B> lb = new ArrayList<>();
List<A> la = lb;   // compile-time error

鉴于Integer是Number的子类型,List和List之间的关系是什么?

The common parent is List<?>.

The common parent is List<?>.

虽然Integer是Number的子类型,但List不是List的子类型,实际上这两个类型是不相关的。 List和List的公共父级是List<?>。

为了创建这些类之间的关系,以便代码可以通过List的元素访问Number的方法,请使用上限通配符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

因为Integer是Number的子类型,numList是Number对象的列表,所以intList(Integer对象列表)和numList之间存在一个关系。 下图显示了使用上下两个有界通配符声明的几个List类之间的关系。

A hierarchy of several generic List class declarations.

A hierarchy of several generic List class declarations.

通配符捕获和辅助方法

在某些情况下,编译器推断通配符的类型。 例如,一个列表可以被定义为List<?>,但是当评估一个表达式时,编译器从代码中推断出一个特定的类型。 这种情况被称为通配符捕获(wildcard capture)。

大多数情况下,除了当您看到包含短语“capture of”的错误消息时,您不必担心通配符捕获。

WildcardError示例在编译时会产生捕获错误:

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

在此示例中,编译器将i输入参数处理为Object类型。 当foo方法调用List.set(int, E)时,编译器无法确认被插入到列表中的对象的类型,并产生错误。 当发生这种类型的错误时,通常意味着编译器认为您将错误的类型分配给变量。 因为这个原因,泛型被添加到Java语言中 - 在编译时强制执行类型安全。

通过Oracle JDK 7 javac实现进行编译时,WildcardError示例生成以下错误:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
    i.set(0, i.get(0));
     ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

在这个例子中,代码试图执行一个安全的操作,所以如何解决编译器错误? 你可以通过编写一个捕获通配符的私有辅助方法(private helper method)来修复它。 在这种情况下,可以通过创建私有辅助方法fooHelper来解决问题,如WildcardFixed所示:

public class WildcardFixed {

    void foo(List<?> i) {
        fooHelper(i);
    }


    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }

}

由于辅助方法,编译器使用推断来确定T是调用捕获的变量CAP#1。 现在编译成功。

按照惯例,辅助方法(helper methods)通常被命名为originalMethodNameHelper。

现在考虑一个更复杂的例子WildcardErrorBad:

import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}

在这个例子中,代码正在尝试一个不安全的操作。 例如,请考虑以下对swapFirst方法的调用:

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);

List和List都满足List<? extends Number>的条件,从Integer值列表中取一个项目并尝试将其放入Double值列表显然是不正确的。

使用Oracle的JDK javac编译器编译代码会产生以下错误:

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
      l2.set(0, temp);      // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
        i.set(0, i.get(0));
         ^
  required: int,CAP#1
  found: int,Object
  reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
  where E is a type-variable:
    E extends Object declared in interface List
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
3 errors

没有辅助方法来解决这个问题,因为代码根本就是错误的。

通配符使用指南

学习使用泛型进行编程时,更令人困惑的一个方面是确定何时使用上限通配符,何时使用下限通配符。 本页面提供了设计代码时要遵循的一些指导原则。

为了讨论的目的,将变量看作提供以下两个函数之一是有帮助的:

  • 一个“In”变量

一个“in”变量将数据提供给代码。 想象一下有两个参数的复制方法:copy(src, dest)。 src参数提供要复制的数据,所以它是“in”参数。

  • 一个“出”变量

“out”变量保存用于其他地方的数据。 在复制示例中,copy(src, dest),dest参数接受数据,所以它是“out”参数。

当然,有些变量既用于“in”也用于“out”的目的 - 这个方案在准则中也有说明。

在决定是否使用通配符时,可以使用“in”和“out”原则,以及使用哪种类型的通配符。 以下列表提供了遵循的准则:

通配符指南(Wildcard Guidelines):

  • 一个“in”变量用一个上限通配符来定义,使用extends关键字。
  • 一个“out”变量用一个下限通配符来定义,使用super关键字。
  • 在可以使用Object类中定义的方法访问“in”变量的情况下,使用无界通配符。
  • 在代码需要以“in”和“out”变量访问变量的情况下,不要使用通配符。

这些准则不适用于方法的返回类型。 应避免使用通配符作为返回类型,因为它强制程序员使用代码来处理通配符。

由List<? extends …>定义的列表可以非正式地认为是只读的,但这不是一个严格的保证。 假设你有以下两个类:

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

考虑下面的代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35));  // compile-time error

因为List是List<? extends NaturalNumber>的子类型,您可以将le分配给ln。 但是,您不能使用ln将自然数(natural number)添加到偶数(even numbers)列表中。 列表中的以下操作是可能的:

  • 你可以添加null。
  • 你可以调用clear。
  • 你可以得到迭代器(iterator)并调用remove。
  • 您可以捕获通配符,并写入您从列表中读取的元素。

你可以看到由List<? extends NaturalNumber>定义的列表在最严格的意义上不是只读的,但是您可能会这样想,因为您无法存储新元素或更改列表中的现有元素。

类型擦除

泛型被引入到Java语言中以在编译时提供更严格的类型检查,并支持泛型编程。 为了实现泛型,Java编译器将类型擦除(type erasure)应用于:

  • 如果类型参数是无界的(unbounded),则将泛型类型中的所有类型参数替换为它们的边界或Object。 所产生的字节码因此只包含普通的类,接口和方法。
  • 如果需要,插入类型强制转换(type casts)可以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

类型擦除确保没有为参数化类型创建新的类; 因此,泛型不会导致运行时开销。

擦除泛型类型

在类型擦除过程中,Java编译器擦除所有类型参数,并在类型参数有界时用它的第一个边界替换每个类型参数;如果类型参数是无界的,那么它将替换为Object。

考虑以下泛型类,它表示单向链表(singly linked list)中的一个节点(node):

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

由于类型参数T是无界的,因此Java编译器将其替换为Object:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

在下面的例子中,泛型Node类使用了一个有界的类型参数:

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Java编译器用第一个有界类(the first bound class)替换有界的类型参数T,Comparable:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

泛型方法的擦除

Java编译器也会擦除泛型方法参数中的类型参数。 考虑下面的一般方法:

// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
    int cnt = 0;
    for (T e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

由于T是无界的,因此Java编译器将其替换为Object:

public static int count(Object[] anArray, Object elem) {
    int cnt = 0;
    for (Object e : anArray)
        if (e.equals(elem))
            ++cnt;
        return cnt;
}

假设定义了以下类:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }

你可以写一个泛型方法来绘制不同的形状:

public static <T extends Shape> void draw(T shape) { /* ... */ }

Java编译器用Shape替换T:

public static void draw(Shape shape) { /* ... */ }

类型擦除和桥接方法的影响

有时类型擦除导致您可能没有预料到(anticipated)的情况。 以下示例显示如何发生这种情况。 该示例(在“桥接方法(Bridge Methods)”中进行了说明)显示了编译器有时会如何创建称为桥接方法的合成方法(synthetic method),作为类型擦除过程的一部分。

鉴于以下两类:

public class Node<T> {

    public T data;

    public Node(T data) { this.data = data; }

    public void setData(T data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

考虑下面的代码:

MyNode mn = new MyNode(5);
Node n = mn;            // A raw type - compiler throws an unchecked warning
n.setData("Hello");     
Integer x = mn.data;    // Causes a ClassCastException to be thrown.

类型擦除后,这段代码变成:

MyNode mn = new MyNode(5);
Node n = (MyNode)mn;         // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.

这是执行代码时发生的情况:

  • n.setData("Hello");导致方法setData(Object)在类MyNode的对象上执行。 (MyNode类从Node继承setData(Object))。
  • 在setData(Object)的主体中,由n引用的对象的数据字段被分配给一个String。
  • 可以访问通过mn引用的同一对象的数据字段,并且期望它是一个整数(因为mn是一个Node的MyNode)。
  • 尝试将String分配给Integer会导致Java编译器在赋值时插入的强制转换的ClassCastException。

桥接方法

编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个称为桥接方法(bridge method)的合成方法(synthetic method),作为类型擦除过程的一部分。 您通常不需要担心桥接方法,但是如果出现在堆栈轨迹(stack trace)中,您可能会感到困惑。

类型擦除后,Node和MyNode类成为:

public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
        System.out.println("Node.setData");
        this.data = data;
    }
}

public class MyNode extends Node {

    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

类型擦除后,方法签名不匹配。 Node方法变为setData(Object),MyNode方法变成setData(Integer)。 因此,MyNode setData方法不会重载Node setData方法。

为了解决这个问题并在类型擦除之后保留泛型类型的多态性,Java编译器生成一个桥接方法以确保子类型按预期工作。 对于MyNode类,编译器为setData生成以下桥接方法:

class MyNode extends Node {

    // Bridge method generated by the compiler
    //
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    // ...
}

正如你所看到的,在类型擦除之后,具有与Node类的setData方法相同的方法签名的桥接方法委托给原始的setData方法。

不可确定的类型

类型擦除(Type Erasure)部分讨论编译器除去与类型参数(type parameters)和类型参数(type arguments)相关的信息的过程。 类型擦除具有与可变参数(也称为可变参数 - varargs)方法相关的结果,其可变参数形式参数具有不可确定的类型(non-reifiable type)。

不可确定的类型

可重用类型(reifiable type)是一种类型,其类型信息在运行时完全可用。 这包括基本类型,非泛型类型,原始类型(raw type)和无界通配符的调用。

不可修饰类型(Non-reifiable types)是在编译时通过类型擦除来删除信息的类型 - 调用未定义为无界通配符的泛型类型。 不可修饰的类型在运行时没有提供所有的信息。 不可修饰的类型的例子是List和List; JVM在运行时无法区分这些类型。 如泛型限制所示,在某些情况下,不可修饰类型不能使用:例如在instanceof表达式中,或者在数组中。

堆污染

当参数化类型的变量引用不是该参数化类型的对象时,会发生堆污染(Heap pollution)。 如果程序在编译时执行了一些导致未经检查的警告的操作,则会出现这种情况。 如果在编译时(在编译时类型检查规则的限制内)或在运行时生成未经检查的警告(unchecked warning),涉及参数化类型(例如,强制转换或方法调用)的操作的正确性不能验证。 例如,混合原始类型和参数化类型时,或执行未经检查的强制转换时会发生堆污染。

在正常情况下,当所有代码被同时编译时,编译器会发出一个未经检查的警告,以引起您注意潜在的堆污染。 如果分别编译代码的各个部分,则很难检测到堆污染的潜在风险。 如果你确保你的代码编译没有警告,那么不会发生堆污染。

具有不可确定形式参数的可变参数方法的潜在弱点

包含可变参数输入参数的泛型方法可能导致堆积污染。

考虑下面的ArrayBuilder类:

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;     // Valid
    objectArray[0] = Arrays.asList(42);
    String s = l[0].get(0);       // ClassCastException thrown here
  }

}

以下示例HeapPollutionExample使用ArrayBuiler类:

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists =
      new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists,
      stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

编译时,ArrayBuilder.addToList方法的定义会产生以下警告:

warning: [varargs] Possible heap pollution from parameterized vararg type T

当编译器遇到可变参数方法(varargs method)时,它将可变参数形式参数转换为数组。 但是,Java编程语言不允许创建参数化类型的数组。 在ArrayBuilder.addToList方法中,编译器将可变参数形式参数T… 元素转换为形式参数T[]元素,一个数组。 但是,由于类型擦除,编译器将可变参数形式参数转换为Object[]元素。 因此,堆污染有可能。

以下语句将可变参数形式参数l分配给Object数组objectArgs:

Object[] objectArray = l;

这一说法可能会引入堆污染。 与可变参数形式参数l的参数化类型匹配的值可以分配给变量objectArray,因此可以分配给l。 但是,编译器不会在此语句中生成未经检查的警告。 编译器在将可变参数形式参数List… l转换为形式参数List[] l时,已经产生警告。 这个声明是有效的; 变量l的类型是List[],它是Object[]的子类型。

因此,如果将任何类型的List对象分配给objectArray数组的任何数组组件,则编译器不会发出警告或错误,如以下语句所示:

objectArray[0] = Arrays.asList(42);

此语句使用包含Integer类型的一个对象的List对象分配给objectArray数组的第一个数组组件。

假设您使用以下语句调用ArrayBuilder.faultyMethod:

ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

在运行时,JVM在以下语句中抛出ClassCastException:

// ClassCastException thrown here
String s = l[0].get(0);

存储在变量l的第一个数组组件中的对象具有List类型,但是该语句期望List类型的对象。

防止具有不可确定的形式参数的可变参数方法的警告

如果声明了一个参数为可变参数类型的可变参数方法,并且由于不正确地处理可变参数形式参数,确保方法主体不抛出ClassCastException或其他类似异常,则可以防止编译器通过将以下注释添加到静态和非构造函数方法声明中,为这些类型的可变参数方法生成:

@SafeVarargs

@SafeVarargs注释是方法合同的文档化部分; 该注解断言该方法的实现不会不当地处理可变参数形式参数。

通过在方法声明中添加以下内容,也可能(虽然不太理想)压制这样的警告:

@SuppressWarnings({"unchecked", "varargs"})

但是,这种方法不会抑制该方法的调用网站产生的警告。

对泛型的限制

不能实例化具有基本类型的泛型类型

考虑以下参数化类型:

class Pair<K, V> {

    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    // ...
}

创建Pair对象时,不能将基本类型替换为类型参数K或V:

Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error

您只能用非基本类型替换类型参数K和V:

Pair<Integer, Character> p = new Pair<>(8, 'a');

请注意,Java编译器自动封装(autoboxing)8到Integer.valueOf(8)和’a’到Character(‘a’):

Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

无法创建类型参数的实例

你不能创建一个类型参数的实例。 例如,下面的代码导致编译时错误:

public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

作为解决方法,您可以通过反射来创建类型参数的对象:

public static <E> void append(List<E> list, Class<E> cls) throws Exception {
    E elem = cls.newInstance();   // OK
    list.add(elem);
}

您可以按如下方式调用append方法:

List<String> ls = new ArrayList<>();
append(ls, String.class);

不能声明类型是类型参数的静态字段

类的静态字段是由类的所有非静态对象共享的类级变量。 因此,不允许使用类型参数的静态字段。 考虑以下类:

public class MobileDevice<T> {
    private static T os;

    // ...
}

如果允许类型参数的静态字段,那么下面的代码会被混淆:

MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态字段os是由phone,pager和pc共享的,所以os的实际类型是什么? 它不能同时是Smartphone,Pager和TabletPC。 因此,您不能创建类型参数的静态字段。

不能对参数化类型使用cast或instanceof

因为Java编译器会删除泛型代码中的所有类型参数,所以无法验证运行时正在使用的泛型类型的参数化类型:

public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

传递给rtti方法的参数化类型的集合是:

S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }

运行时不会跟踪类型参数,所以它不能区分ArrayList和ArrayList。 你可以做的最多的是使用一个无界的通配符来验证列表是一个ArrayList:

public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

通常情况下,除非通过无界通配符进行参数化,否则不能强制转换为参数化类型。 例如:

List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // compile-time error

但是,在某些情况下,编译器知道类型参数总是有效的并且允许转换。 例如:

List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1;  // OK

不能创建参数化类型的数组

您不能创建参数化类型的数组。 例如,下面的代码不能编译:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

下面的代码演示了将不同类型插入到数组中时会发生什么:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

如果你用一个泛型列表尝试同样的事情,会出现一个问题:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

如果允许参数化列表数组,则以前的代码将无法抛出所需的ArrayStoreException。

无法创建,捕捉或抛出参数化类型的对象

泛型类不能直接或间接地扩展Throwable类。 例如,以下类将不会编译:

// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

一个方法不能捕获一个类型参数的实例:

public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}

但是,您可以在throws子句中使用类型参数:

class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

不能重载每个重载的形式参数类型擦除到相同的原始类型的方法

一个类不能有两个重载方法,在类型擦除之后将具有相同的签名。

public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

重载将全部共享相同的类文件表示,并将生成编译时错误。

软件包(Package)是 Java 编程语言的一个功能,可帮助您组织和构建您的类及其彼此之间的关系。

创建和使用软件包

为了使类型更易于查找和使用,避免命名冲突并控制访问,程序员将相关类型的组捆绑到包中。

**定义:**包(package)是一组提供访问保护和名称空间管理的相关类型。请注意,类型是指类,接口,枚举和注释类型。枚举(Enumerations)和注解类型分别是类和接口的特殊类型,所以在本课中经常将类型称为类和接口。

作为Java平台一部分的类型是通过函数捆绑类的各种包的成员:位于java.lang中基础类,位于java.io中用于读写的类(输入和输出),依此类推。你也可以把你的类型放在包里。

假设您编写了一组代表图形对象的类,例如圆形,矩形,线条和点。您还可以编写一个接口Draggable,如果可以用鼠标拖动这些接口,就可以实现这些接口。

//in the Draggable.java file
public interface Draggable {
    ...
}

//in the Graphic.java file
public abstract class Graphic {
    ...
}

//in the Circle.java file
public class Circle extends Graphic
    implements Draggable {
    . . .
}

//in the Rectangle.java file
public class Rectangle extends Graphic
    implements Draggable {
    . . .
}

//in the Point.java file
public class Point extends Graphic
    implements Draggable {
    . . .
}

//in the Line.java file
public class Line extends Graphic
    implements Draggable {
    . . .
}

您应该将这些类和接口捆绑在一个包中,原因有很多,其中包括:

  • 您和其他程序员可以轻松确定这些类型是相关的。
  • 您和其他程序员知道在哪里可以找到可以提供图形相关功能的类型。
  • 您的类型的名称不会与其他包中的类型名称冲突,因为该包会创建一个新的名称空间。
  • 您可以允许包中的类型具有不受限制的访问权限,但仍然限制包之外的类型的访问权限。

创建一个包

要创建一个包,你需要为这个包选择一个名称,并在每个包含类型(类,接口,枚举和注解)的且你想包含在包中的源文件的顶部放置一个包含该名称的package语句。

包语句(例如,package graphics;)必须是源文件中的第一行。每个源文件中只能有一个包语句,并且它适用于文件中的所有类型。

**注意:**如果您将多个类型放在一个源文件中,则只有一个可以是public,并且它必须与源文件具有相同的名称。例如,您可以在Circle.java文件中定义public class Circle,在文件Draggable.java中定义public interface Draggable,在Day.java文件中定义public enum Day,等等。

您可以将非公开类型与公共类型包含在同一个文件中(除非非公开类型很小且与公共类型密切相关,否则这是非常不鼓励的),但只有公共类型可以从包外访问。所有的顶级非公开类型都将是包私有的(package private)。

如果将上一节中列出的图形界面和类放在名为graphics的包中,则需要六个源文件,如下所示:

//in the Draggable.java file
package graphics;
public interface Draggable {
    . . .
}

//in the Graphic.java file
package graphics;
public abstract class Graphic {
    . . .
}

//in the Circle.java file
package graphics;
public class Circle extends Graphic
    implements Draggable {
    . . .
}

//in the Rectangle.java file
package graphics;
public class Rectangle extends Graphic
    implements Draggable {
    . . .
}

//in the Point.java file
package graphics;
public class Point extends Graphic
    implements Draggable {
    . . .
}

//in the Line.java file
package graphics;
public class Line extends Graphic
    implements Draggable {
    . . .
}

如果你不使用package语句,你的类型将以未命名的包结束。 一般来说,一个未命名的软件包只适用于小型或临时应用程序,或者当您刚刚开始开发过程时。 否则,类和接口属于命名包。

命名一个包

随着全世界的程序员使用Java编程语言编写类和接口,许多程序员可能会为不同的类型使用相同的名称。实际上,前面的例子只是这样做的:当java.awt包中已经有一个Rectangle类时,它定义了一个Rectangle类。尽管如此,编译器允许两个类具有相同的名称(如果它们位于不同的包中)。每个Rectangle类的完全限定名称包括包名称。也就是说,图形包中的Rectangle类的完全限定名是graphics.Rectangle,而java.awt包中的Rectangle类的完全限定名是java.awt.Rectangle。

除非两个独立的程序员使用相同的名字来包装它,什么防止这个问题?惯例。

命名约定

包名称全部写成小写,以避免与类或接口的名称冲突。

公司使用反向的互联网域名来开始他们的软件包名称,例如,由example.com的程序员创建的名为mypackage的软件包的com.example.mypackage。

在单个公司内部发生的名称冲突需要在该公司内按照惯例进行处理,也许通过在公司名称之后包含区域或项目名称(例如,com.example.region.mypackage)来处理。

Java语言中的包本身是以java开头的。或者javax。

在某些情况下,互联网域名可能不是有效的软件包名称。如果域名包含连字符或其他特殊字符,如果包名称以非法用作Java名称开头的数字或其他字符开头,或者包名称包含保留的Java关键字,如“int”,会发生无效名称。在这种情况下,建议的约定是增加一个下划线。例如:

Legalizing Package Names

Domain Name Package Name Prefix
hyphenated-name.example.org org.example.hyphenated_name
example.int int_.example
123name.example.com com.example._123name

使用包成员

组成包的类型被称为包成员(package members)。

要从包外使用public包成员,您必须执行以下任一操作:

  • 以其完全限定名称(fully qualified name)引用该成员
  • 导入包成员
  • 导入成员的整个包

每一个都适用于不同的情况,如下面的部分所述。

以其限定名称引用包成员

到目前为止,本教程中的大多数示例都通过它们的简单名称(如Rectangle和StackOfInts)来引用类型。如果您正在编写的代码与该成员位于同一个包中,或者该成员已被导入,则可以使用包成员的简单名称。

但是,如果您试图使用不同软件包中的成员,并且该软件包尚未导入,则必须使用该成员的完全限定名称,其中包含包名称。以上示例中的图形包中声明的Rectangle类的完整名称。

graphics.Rectangle

您可以使用此限定名称来创建graphics.Rectangle的实例:

graphics.Rectangle myRect = new graphics.Rectangle();

限定名称可以不经常使用。 但是,当重复使用名称时,反复输入名称变得单调乏味,代码变得难以阅读。 或者,您可以导入(import)成员或其包,然后使用它的简单名称。

导入包成员

要将特定成员导入当前文件,请在任何类型定义之前但在package语句之后(如果有)定义一个import语句。 下面介绍如何从上一节创建的图形包中导入Rectangle类。

import graphics.Rectangle;

现在你可以通过简单的名字来引用Rectangle类。

Rectangle myRectangle = new Rectangle();

如果仅使用图形包中的几个成员,则此方法运行良好。 但是,如果您使用包中的许多类型,则应该导入整个包。

导入整个包

要导入包含在特定包中的所有类型,请使用带有星号(*)通配符的导入语句。

import graphics.*;

现在你可以用简单的名字来引用图形包中的任何类或接口。

Circle myCircle = new Circle();
Rectangle myRectangle = new Rectangle();

import语句中的星号(asterisk)只能用来指定包中的所有类,如下所示。 它不能用于匹配包中类的子集。 例如,以下内容不匹配以A开头的图形包中的所有类。

// does not work
import graphics.A*;

相反,它会生成一个编译器错误。 使用import语句,通常只导入一个包成员或整个包。

**注意:**另一种不常见的导入形式允许您导入封闭类的公共嵌套类(the public nested classes of an enclosing class)。 例如,如果graphics.Rectangle类包含有用的嵌套类(例如Rectangle.DoubleWide和Rectangle.Square),则可以使用以下两个语句导入Rectangle及其嵌套类。

import graphics.Rectangle;
import graphics.Rectangle.*;

请注意,第二个导入语句不会导入Rectangle。

另一个不太常见的导入形式是静态导入语句,将在本节末尾讨论。

为了方便,Java编译器自动为每个源文件导入两个完整的包:(1)java.lang包和(2)当前包(当前文件包)。

包的表观层次结构

起初,包似乎是分层的,但不是。 例如,Java API包含一个java.awt包,一个java.awt.color包,一个java.awt.font包以及许多以java.awt开头的其他包。 但是java.awt包中不包含java.awt.color包,java.awt.font包和其他java.awt.xxxx包。 前缀java.awt(Java抽象窗口工具包)用于许多相关的包,以使关系明显,但不显示包含。

导入java.awt.*将导入java.awt包中的所有类型,但不导入java.awt.color,java.awt.font或任何其他java.awt.xxxx包。 如果打算使用java.awt.color中的类和其他类型以及java.awt中的类和其他类型,则必须导入包含其所有文件的两个包:

import java.awt.*;
import java.awt.color.*;

名称歧义

如果一个包中的成员与另一个包中的成员共享其名称,并且这两个包都被导入,则必须以其限定名称引用每个成员。 例如,图形包定义了一个名为Rectangle的类。 java.awt包也包含一个Rectangle类。 如果graphics和java.awt都已导入,则以下内容不明确(ambiguous)。

Rectangle rect;

在这种情况下,您必须使用该成员的完全限定名来准确指出您想要的Rectangle类。 例如,

graphics.Rectangle rect;

静态导入声明

有些情况下需要频繁访问一个或两个类的静态final字段(常量)和静态方法。 反复添加这些类的名称可能会导致代码混乱。 静态导入(static import)语句为您提供了一种导入要使用的常量和静态方法的方法,这样您就不需要在类的名称前加上前缀。

java.lang.Math类定义了PI常量和许多静态方法,包括计算正弦,余弦,正切,平方根,最大值,最小值,指数等等的方法。 例如,

public static final double PI 
    = 3.141592653589793;
public static double cos(double a)
{
    ...
}

通常,要使用来自其他类的这些对象,请按照以下方式在类名前缀。

double r = Math.cos(Math.PI * theta);

您可以使用静态导入语句来导入java.lang.Math的静态成员,以便您不需要在类名Math加前缀。 Math的静态成员可以单独导入:

import static java.lang.Math.PI;

或作为一个组:

import static java.lang.Math.*;

一旦他们被导入,静态成员可以不使用限定名。 例如,以前的代码片段将变成:

double r = cos(PI * theta);

显然,你可以编写你自己的包含你经常使用的常量和静态方法的类,然后使用静态导入语句。 例如,

import static mypackage.MyConstants.*;

**注意:**使用静态导入非常节省。 过度使用静态导入会导致难以读取和维护的代码,因为代码的读者不知道哪个类定义了特定的静态对象。 正确使用,静态导入通过删除类名称重复使代码更具可读性。

管理源文件和类文件

Java平台的许多实现都依赖分层文件系统来管理源文件和类文件,尽管“Java语言规范”并不要求这样做。 策略如下。

将类,接口,枚举或注释类型的源代码放在一个文本文件中,该文件的名称是类型的简单名称,扩展名是.java。 例如:

//in the Rectangle.java file 
package graphics;
public class Rectangle {
   ... 
}

然后,将源文件放在名称反映类型所属包的名称的目录中:

.....\graphics\Rectangle.java

假定Microsoft Windows文件名分隔符反斜杠(对于UNIX,使用正斜杠),包成员的限定名称和文件的路径名是并行的。

  • 类名 - graphics.Rectangle
  • 路径名到文件 - graphics\Rectangle.java

正如您应该记得的,按照惯例,一家公司使用其反向因特网域名作为其软件包名称。 Example公司的互联网域名是example.com,它的所有包名都会以com.example开头。 包名称的每个组件都对应一个子目录。 因此,如果Example公司有一个包含Rectangle.java源文件的com.example.graphics包,它将包含在一系列子目录中,如下所示:

....\com\example\graphics\Rectangle.java

编译源文件时,编译器会为其中定义的每种类型创建一个不同的输出文件。 输出文件的基本名称是类型的名称,其扩展名是.class。 例如,如果源文件是这样的

//in the Rectangle.java file
package com.example.graphics;
public class Rectangle {
      . . . 
}

class Helper{
      . . . 
}

那么编译的文件将位于:

<path to the parent directory of the output files>\com\example\graphics\Rectangle.class
<path to the parent directory of the output files>\com\example\graphics\Helper.class

和.java源文件一样,编译后的.class文件应该放在一系列反映包名称的目录中。 但是,.class文件的路径不必与.java源文件的路径相同。 您可以分别安排源和目录目录,如下所示:

<path_one>\sources\com\example\graphics\Rectangle.java

<path_two>\classes\com\example\graphics\Rectangle.class

通过这样做,您可以将classes目录提供给其他程序员,而不会泄露您的源代码。 您还需要以这种方式管理源文件和类文件,以便编译器和Java虚拟机(JVM)可以找到您的程序使用的所有类型。

类目录<path_two>\classes的完整路径称为类路径(class path),并使用CLASSPATH系统变量进行设置。 编译器和JVM都通过将包名添加到类路径来构建.class文件的路径。 例如,如果

<path_two>\classes

是你的类路径,包名是

com.example.graphics,

那么编译器和JVM就会查找.class文件

<path_two>\classes\com\example\graphics.

类路径可能包含多个路径,用分号(Windows)或冒号(UNIX)分隔。 默认情况下,编译器和JVM搜索当前目录以及包含Java平台类的JAR文件,以便这些目录自动位于类路径中。

设置CLASSPATH系统变量

要显示当前的CLASSPATH变量,请在Windows和UNIX(Bourne shell)中使用这些命令:

In Windows:   C:\> set CLASSPATH
In UNIX:      % echo $CLASSPATH

要删除CLASSPATH变量的当前内容,请使用以下命令:

In Windows:   C:\> set CLASSPATH=
In UNIX:      % unset CLASSPATH; export CLASSPATH

要设置CLASSPATH变量,请使用以下命令(例如):

To set the CLASSPATH variable, use these commands (for example):

In Windows:   C:\> set CLASSPATH=C:\users\george\java\classes
In UNIX:      % CLASSPATH=/home/george/java/classes; export CLASSPATH

参考

  1. Learning the Java Language