Java17 快速语法参考(二)
十七、常量
Java 中的变量可以通过在数据类型前添加final关键字而变成常量。这个修饰符意味着变量一旦被设置就不能被重新分配,任何这样做的尝试都会导致编译时错误。
局部常量
通过应用 final 关键字,可以将局部变量声明为常量。这样的常量必须总是在声明的同时被初始化。Java 对常量的命名约定是全部使用大写字母,并用下划线分隔单词。
final double PI = 3.14;
常量字段
类和实例变量可以声明为final,如下例所示:
class MyClass
{
final double E = 2.72;
static final double C = 3e8;
final static double D = 1.23; // alternative order
}
与局部常量相反,常量字段不必在声明时赋值。常量实例字段可以选择在构造器或实例初始化块中赋值,而常量静态字段可以通过使用静态初始化块来赋值。如果需要计算常量的值,并且单个代码行无法容纳该值,那么这些可选的赋值可能会很有用。
class MyClass
{
final double E;
final double PI;
static final double C;
public MyClass() { E = 2.72; }
{ PI = 3.14; }
static { C = 3e8; }
}
恒定方法参数
另一个可能应用final修饰符的地方是方法参数,使它们不可更改。这样做向其他开发人员提供了一个信号,即该方法不会修改传递给它的参数。
void f(final String A) {}
编译时和运行时常量
像大多数其他语言一样,Java 既有编译时常量,也有运行时常量。然而,在 Java 中只有类常量可以是编译时常量,并且只有在编译时它们的值是已知的。对final的所有其他使用都将创建运行时常量。对于编译时常量,编译器会用它的值替换代码中任何地方的常量名。因此,它们比运行时常量更快,运行时常量在程序运行之前不会被设置。不过,运行时常量可以被赋予动态值,这些值在不同的程序运行之间可能会有所不同。
class MyClass
{
// Compile-time constant (static and known at compile-time)
final static double C = 3e8;
// Run-time constant (not static)
final double E = 2.72;
// Run-time constant (not known at compile-time)
final static int RND = (new
java.util.Random()).nextInt();
}
不变准则
一般来说,如果不需要重新分配,最好总是将变量声明为final,将常量字段声明为static final。这确保了字段和变量不会在程序的任何地方被错误地更改,这反过来有助于防止错误。
十八、接口
类型用于指定使用接口的类必须实现的方法。这些方法是用关键字interface创建的,后跟一个名称和一个代码块。它们的命名约定与类相同,每个单词的第一个字母大写。
interface MyInterface {}
当一个接口没有嵌套在另一个类型中时,它的访问级别可以是 package-private 或 public,就像任何其他顶级成员一样。
界面成员
首先,接口的代码块可以包含实例方法的签名。这些方法没有任何实现。相反,他们的身体被分号代替。默认情况下,接口成员具有公共访问权限,因此可以省略这个修饰符。
interface MyInterface {
int myMethod(); // method signature
}
接口可以包含的第二个成员是常量。在接口中创建的任何字段都将被隐式声明为static final,因此这些修饰符也可以被省略。
interface MyInterface {
int c = 10; // constant
}
除了方法签名和常量,接口还可以包含嵌套的包含类型,如类或其他接口:
interface MyInterface
{
// Types
class Class {}
interface Interface {}
enum Enum {}
}
界面示例
下面的例子展示了一个名为Comparable的接口,它有一个名为compare的方法:
interface Comparable
{
int compare(Object o);
}
下面的类使用类名后的关键字implements实现了这个接口。按照惯例,如果类有 implements 子句,那么 implements 子句放在extends子句之后。请注意,尽管一个类只能从一个超类继承,但它可以通过在逗号分隔的列表中指定接口来实现任意数量的接口。
class Circle implements Comparable
{
public int r;
// ...
}
因为Circle实现了Comparable,所以它必须定义compare方法。对于这个类,该方法将返回圆半径之间的差值。实现的方法必须是公共的,并且必须与接口中定义的方法具有相同的签名。
class Circle implements Comparable
{
public int r;
@Override
public int compare(Object o) {
return r - ( (Circle)o ).r;
}
}
功能界面
演示了接口的第一个用途,即定义一个类可以共享的特定功能。它使得在不知道类的实际类型的情况下使用接口成员成为可能,这个概念叫做多态。为了说明这一点,下一个例子展示了一个简单的方法,它接受两个Comparable对象并返回最大的一个。这个方法适用于任何实现了Comparable接口的类,因为这个方法只使用通过那个接口公开的功能。
public static Object largest(Comparable a, Comparable b)
{
return (a.compare(b) > 0) ? a : b;
}
类接口
使用接口的第二种方法是为一个类提供一个实际的接口,通过这个接口可以使用这个类。下面的例子为MyClass定义了一个名为MyInterface的接口。该接口仅包括使用MyClass的程序员可能需要的功能。
interface MyInterface
{
void exposed();
}
class MyClass implements MyInterface
{
@Override
public void exposed() {}
public void hidden() {}
}
然后,接口类型用于保存实现类,因此只能通过该接口看到该类:
public static void main(String[] args)
{
MyInterface i = new MyClass();
}
这种抽象提供了两个好处。首先,它使其他程序员更容易使用该类,因为他们现在只能访问相关的方法。其次,它使类更加灵活,因为只要遵循接口,它的实现就可以改变,而不会被使用该类的其他程序员注意到。
接口类
如前所述,一个接口可以包含嵌套类型,比如类。与方法不同,这些类型是在接口内部实现的。例如,这可以用于提供一个包含对实现类有用的静态方法的类。这些嵌套类型仅对实现接口的类可见,对这些类的对象不可见。
interface MyInterface
{
class HelperClass {
public static void helperMethod() {}
}
}
默认接口方法
Java 8 增加了在接口中定义默认方法的能力。这种方法是使用关键字default指定的,然后可以在接口中包含一个实现。
interface MyInterface
{
default void defaultMethod() {
System.out.println("default");
}
}
将使用默认方法,除非它被实现类覆盖。这提供了一种向后兼容的方式来向接口添加新方法,而不会破坏使用该接口的现有类。
public class MyApp implements MyInterface
{
public static void main(String[] args) {
MyInterface i = new MyApp();
i.defaultMethod(); // "default"
}
}
静态接口方法
Java 8 中引入的另一个特性是静态接口方法。与静态类方法类似,这些方法属于接口,只能从接口上下文中调用。
interface MyInterface
{
public static void staticMethod() {
System.out.println("static");
}
}
class MyApp
{
public static void main(String[] args) {
MyInterface.staticMethod(); // "static"
}
}
从 Java 9 开始,静态接口方法可以进行私有访问。这使得冗长的默认接口方法可以跨私有静态接口方法进行拆分,从而减少代码重复。
interface MyInterface
{
private static String getString() {
return "string";
}
default void printString() {
System.out.println(getString());
}
}
十九、抽象
一个抽象类提供了一个部分实现,其他类可以在此基础上构建。当一个类被声明为abstract时,意味着除了普通的类成员之外,它还可以包含必须在子类中实现的不完整的方法。这些方法没有实现,只指定了它们的签名,而它们的主体被分号替换。
abstract class Shape
{
public int x = 100, y = 100;
public abstract int getArea();
}
抽象类示例
如果一个名为Rectangle的类继承了抽象类Shape,那么Rectangle将被强制覆盖抽象方法getArea。唯一的例外是如果Rectangle也被声明为abstract,在这种情况下,它不必实现任何抽象方法。
class Rectangle extends Shape
{
@Override public int getArea() {
return x * y;
}
}
抽象类不能被实例化,但它可以用来保存其子类的实例:
public class MyApp
{
public static void main(String[] args) {
Shape s = new Rectangle();
}
}
尽管抽象类不能被实例化,但它可能有构造器,可以使用super关键字从子类的构造器中调用这些构造器:
abstract class Shape
{
public int x = 100, y = 100;
public Shape(int a, int b) {
x = a;
y = b;
}
}
class Rectangle extends Shape
{
public Rectangle(int a, int b) {
super(a,b);
}
}
public class MyApp
{
public static void main(String[] args) {
Rectangle s = new Rectangle(5, 10);
}
}
抽象类和接口
抽象类在许多方面类似于接口。它们都可以定义子类必须实现的方法签名,它们都不能被实例化。一个关键的区别是,抽象类可以包含任何抽象或非抽象成员,而接口仅限于抽象成员、嵌套类型和静态常量,以及静态方法、默认方法和私有方法。另一个区别是,一个类可以实现任意数量的接口,但只能从一个类继承,不管是不是抽象的。
接口或者用于定义一个类可以拥有的特定功能,或者为使用该类的其他开发人员提供一个接口。相反,抽象类用于提供部分类实现,留给子类来完成。当子类有一些共同的功能,但也有一些必须为每个子类不同地实现的功能时,这是有用的。
二十、枚举类型
一个枚举,或枚举,是一个由固定的命名常量列表组成的类型。要创建一个,使用enum关键字,后跟一个名称和一个代码块,代码块包含一个以逗号分隔的常量元素列表。枚举的访问级别与类的访问级别相同。默认情况下是 Package-private,但是如果在同名文件中声明,也可以将其设置为 public。与类一样,枚举也可以包含在类中,然后可以设置为任何访问级别。
enum Speed
{
STOP, SLOW, NORMAL, FAST
}
刚才显示的枚举类型的对象可以保存四个定义的常量中的任何一个。枚举常量就像类的静态字段一样被访问。
Speed s = Speed.SLOW;
switch语句提供了一个枚举何时有用的好例子。与使用普通常量相比,枚举的优点是允许程序员清楚地指定允许哪些常量值。这提供了编译时类型安全。注意,当在switch语句中使用枚举时,case 标签没有用枚举的名称限定。
public class MyApp
{
public static void main(String args[]) {
Speed s = Speed.NORMAL;
// ...
switch(s) {
case STOP: break;
case SLOW: break;
case NORMAL: break;
case FAST: break;
}
}
}
枚举类
在 Java 中,enum 类型比其他语言(如 C++或 C#)中的 enum 类型更强大。本质上是一种特殊的类,它可以包含一个类可以包含的任何东西。要添加类成员,常量列表必须以分号结束,并且成员必须在常量之后声明。在下面的示例中,一个整数被添加到枚举中,它将保存元素所表示的实际速度。
enum Speed
{
STOP, SLOW, NORMAL, FAST;
public int velocity;
// ...
}
要设置这个字段,还需要添加一个构造器。枚举中的构造器总是私有的,不会像普通类那样被调用。相反,构造器的参数在常量元素之后给出,如下例所示。如果一个Speed枚举对象被赋予常量SLOW,那么参数5将被传递给该枚举实例的构造器。
enum Speed
{
STOP(0), SLOW(5), NORMAL(10), FAST(20);
public int velocity;
private Speed(int s) { velocity = s; }
}
public class MyApp
{
public static void main(String args[]) {
Speed s = Speed.SLOW;
System.out.println(s.velocity); // "5"
}
}
与常规类相比,枚举类型的另一个区别是它们隐式地从java.lang.Enum类扩展而来。除了从这个类继承的成员之外,编译器还会自动给枚举添加两个静态方法,分别是values和valueof。values方法返回枚举中声明的常量元素的数组,valueof返回指定枚举名称的枚举常量。
Speed[] a = Speed.values();
String s = a[0].toString(); // "STOP"
Speed b = Speed.valueOf(s); // Speed.STOP
二十一、异常处理
异常处理允许程序员处理程序中可能出现的意外情况。比如 java.io 包中的FileReader类用来打开一个文件。创建该类的一个实例将会导致 IDE 给出一个提示,提示该类的构造器可能会抛出一个FileNotFoundException。试图运行程序也会导致编译器指出这一点。
import java.io.*;
public class MyClass
{
public static void main(String[] args)
{
// Compile-time error
FileReader file = new FileReader("missing.txt");
}
}
试着接住
为了处理这个编译时错误,必须使用一个try-catch语句来捕获异常。该语句由一个包含可能导致异常的代码的try块和一个或多个catch子句组成。如果try块成功执行,程序将在try-catch语句后继续运行,但如果出现异常,执行将传递给第一个能够处理该异常类型的catch块。
try {
FileReader file = new FileReader("missing.txt");
}
catch(FileNotFoundException e) {}
捕捉块
在前面的例子中,catch块仅被设置为处理FileNotFoundException。如果try块中的代码可以抛出更多种类的异常,并且所有的异常都应该以同样的方式处理,那么可以捕捉更一般的异常,比如所有异常都源自的Exception类本身。这个catch子句将能够处理从这个类继承的所有异常,包括FileNotFoundException。请记住,一个更一般的异常需要在一个更具体的异常之后被捕获。子句必须总是定义一个异常对象。这个对象可以用来获得关于异常的更多信息,比如使用getMessage方法对异常的描述。
catch(FileNotFoundException e) {
System.out.print(e.getMessage());
}
catch(Exception e) {
System.out.print(e.getMessage());
}
从 Java 7 开始,可以使用单个catch块捕获不同类型的多个异常。这有助于避免代码重复,在以相同方式处理多个异常的情况下,不必捕获过于一般化的异常类型。在catch子句中,每个异常用竖线(|)分隔。
catch(IOException | SQLException e) {
// Handle exception
}
最终阻止
作为try-catch语句的最后一个子句,可以添加一个finally块。这个块用于清理分配在try块中的资源,并且无论是否有异常都会执行。在本例中,在try块中打开的文件应该被关闭,但前提是它被成功打开。为了能够从finally子句访问FileReader对象,它必须在try块之外声明。此外,因为close方法也可以抛出异常,所以该方法需要用另一个try-catch块包围。请记住,如果您忘记关闭一个资源对象,Java 的垃圾收集器最终会为您关闭该资源,但是自己关闭它是一个很好的编程实践。
FileReader file = null;
try {
file = new FileReader("missing.txt");
}
catch(FileNotFoundException e) {
System.out.print(e.getMessage());
}
finally {
if (file != null) {
try { file.close(); }
catch(IOException e) {}
}
}
Java 7 增加了资源尝试特性。该特性允许通过在try关键字后的括号中定义资源对象来自动关闭资源对象。为此,资源必须实现java.lang.AutoClosable接口。这个接口仅由close方法组成,该方法在隐式的finally语句中被自动调用。因此,前面的例子可以简化如下:
try(FileReader file = new FileReader("missing.txt")) {
// Read file
}
catch(FileNotFoundException e) {
// Handle exception
}
对于自动关闭,可以包括多个资源对象,用分号分隔。为了提高可读性,Java 9 使得在括号外声明的对象可以被 try-with-resources 语句引用,只要这些资源是最终的或者实际上是最终的。
// Final resource
final FileReader file1 = new FileReader("file1.txt");
// Effectively final resource (never changed)
FileReader file2 = new FileReader("file2.txt");
try(file1; file2) {
// Read files
}
catch(FileNotFoundException e) {
// Handle exception
}
抛出异常
当某个方法无法恢复的情况发生时,它可以生成自己的异常,通知调用者该方法已经失败。它使用throw关键字,后跟一个Throwable类型的新实例。
static void makeException()
{
throw new Throwable("My Throwable");
}
已检查和未检查的异常
Java 中的异常分为两类——检查的和未检查的——这取决于它们是否需要被指定。抛出检查异常的方法(例如,IOException)将不会编译,除非在方法的参数列表后使用throws子句指定它,并且调用方法捕获异常。另一方面,未检查的异常,如ArithmeticException,不必被捕获或指定。请注意,要指定多个异常,异常类型由逗号分隔。
static void MakeException()
throws IOException, FileNotFoundException
{
// ...
throw new IOException("My IO exception");
// ...
throw new FileNotFoundException("File missing");
}
异常层次结构
像 Java 中的大多数其他东西一样,异常是存在于层次结构中的类。这个层次结构的根(在Object下面)是Throwable类,这个类的所有后代都可以被抛出和捕获。从Throwable继承而来的是Error和Exception类。从Error开始下降的类用于指示不可恢复的异常,例如OutOfMemoryError。这些是未检查的,因为一旦它们发生,即使它们被发现,程序员也不可能对它们做任何事情。
从Exception往下是RuntimeExceptions,也是未勾选的。这些异常几乎在任何代码中都可能发生,因此捕捉和指定它们会很麻烦。例如,被零除会抛出一个ArithmeticException,但是用一个try-catch包围每个除法运算会很麻烦。还有一些与检查异常相关的开销,检查这些异常的成本通常超过捕获它们的好处。其他的Exception后代,那些不继承RuntimeExceptions的,都被查了。这些是可以从中恢复的异常,并且必须被捕获和指定。
二十二、装箱和拆箱
在对象中放置一个原始变量被称为装箱。装箱允许在需要对象的地方使用原语。为此,Java 提供了包装器类来实现每个原始类型的装箱,即Byte、Short、Integer、Long、Float、Double、Character和Boolean。例如,一个Integer对象可以保存一个int类型的变量。
int iPrimitive = 5;
Integer iWrapper = new Integer(iPrimitive); // boxing
自然,装箱的反义词是 unboxing ,它将对象类型转换回其原始类型。
iPrimitive = iWrapper.intValue(); // unboxing
包装类属于java.lang包,它总是被导入。当使用包装器对象时,请记住等号运算符(==)检查两个引用是否指向同一个对象,而equals方法用于比较对象表示的值。
Integer x = new Integer(1000);
Integer y = new Integer(1000);
boolean b = (x == y); // false
b = x.equals(y); // true
汽车尾气与汽车尾气排放
Java 5 引入了自动装箱和自动拆箱。这些特性允许原语和它们的包装对象之间的自动转换。
Integer iWrapper = iPrimitive; // autoboxing
iPrimitive = iWrapper; // autounboxing
注意,这只是为了让代码更容易阅读而设计的语法糖。编译器将使用valueOf和intValue方法为您添加必要的代码来装箱和取消装箱原语。
Integer iWrapper = Integer.valueOf(iPrimitive);
iPrimitive = iWrapper.intValue()
原语和包装准则
当不需要对象时,应该使用基本类型。这是因为原语通常比对象更快,内存效率更高。相反,当需要数值但需要对象时,包装器是有用的。例如,要在集合类中存储数值,比如ArrayList<>,就需要包装类。
import java.util.ArrayList;
// ...
java.util.ArrayList<Integer> a = new java.util.ArrayList<>();
a.add(10); // autoboxing
int i = a.get(0); // autounboxing
请记住,如果速度很重要,原语和包装对象之间的转换应该保持较低的速度。任何装箱和取消装箱操作都会带来固有的性能损失。
二十三、泛型
泛型指的是类型参数的使用,它提供了一种定义方法、类和接口的方式,这些方法、类和接口可以操作不同的数据类型。泛型的好处是它们提供了编译时类型安全,并且消除了大多数类型转换的需要。
通用类
泛型类允许类成员使用类型参数。这种类是通过在类名后添加类型参数部分来定义的,该部分包含一个用尖括号括起来的类型参数。类型参数的命名约定是它们应该由一个大写字母组成。通常使用字母T代表型。下面的示例定义了一个泛型容器类,它可以保存泛型类型的单个元素:
// Generic container class
class MyBox<T> { public T box; }
当这个泛型类的对象被实例化时,type 参数必须被替换为实际的数据类型,比如Integer:
MyBox<Integer> iBox = new MyBox<Integer>();
或者,从 Java 7 开始,泛型类可以用一组空的类型参数来实例化。只要编译器能够从上下文中推断(确定)类型参数,这种类型的实例化就是可能的。
MyBox<Integer> iBox = new MyBox<>();
当MyBox的实例被创建时,类定义中的每个类型参数都被替换为传入的类型参数。因此,该对象表现得像一个常规对象,只有一个Integer类型的字段。
iBox.box = 5;
Integer i = iBox.box;
请注意,当从box字段设置或检索存储值时,不需要转换。此外,如果泛型字段被错误地赋值或设置为不兼容的类型,编译器会指出来。
iBox.box = "Hello World"; // compile-time error
String s = iBox.box; // compile-time error
通用方法
通过在方法的返回类型前声明一个类型参数节,可以使方法成为泛型方法。类型参数可以像方法内部的任何其他类型一样使用。您还可以在throws子句中将其用于方法的返回类型及其参数类型。下一个示例显示了一个接受泛型数组参数的泛型类方法,其内容被打印出来。
class MyClass
{
public static <T> void printArray(T[] array)
{
for (T element : array)
System.out.println(element);
}
}
前面显示的类不是泛型的。无论封闭类或接口是否为泛型,方法都可以声明为泛型。构造器也是如此,如下例所示:
public class MyApp
{
private String s;
public <T> MyApp(T t) {
s = t.toString(); // convert to string
}
public static void main(String[] args) {
MyApp o = new MyApp(10);
System.out.println(o.s); // "10"
}
}
调用泛型方法
泛型方法通常像常规(非泛型)方法一样调用,不指定类型参数:
Integer[] iArray = { 1, 2, 3 };
MyClass.printArray(iArray);
在大多数情况下,Java 编译器可以推断出泛型方法调用的类型参数,所以不必包含它。但是如果不是这样,那么需要在方法名之前显式指定类型参数:
MyClass.<Integer>printArray(iArray);
通用接口
用类型参数声明的接口成为泛型接口。泛型接口与常规接口有两个相同的目的:要么创建它们来公开将被其他类使用的类的成员,要么强制一个类实现特定的功能。实现泛型接口时,必须指定类型参数。泛型接口可以由泛型和非泛型类实现:
// Generic functionality interface
interface IGenericCollection<T>
{
void store(T t);
}
// Non-generic class implementing generic interface
class Box implements IGenericCollection<Integer>
{
private Integer myBox;
public void store(Integer i) { myBox = i; }
}
// Generic class implementing generic interface
class GenericBox<T> implements IGenericCollection<T>
{
private T myBox;
public void store(T t) { myBox = t; }
}
泛型类型参数
泛型的传入类型参数可以是类类型、接口类型或其他泛型类型参数,但不能是基元类型。泛型可以定义多个类型参数,方法是在逗号分隔的列表中的尖括号之间添加多个类型参数。请记住,括号中的每个参数都必须是唯一的。
class MyClass<T, U> {}
如果泛型定义了多个类型参数,则在使用泛型时,需要指定相同数量的类型参数。
MyClass<Integer, Float> m = new MyClass<>();
通用变量用法
泛型只是 Java 中的一个编译时构造。在编译器检查了与泛型变量一起使用的类型是正确的之后,它将从泛型代码中删除所有类型参数和实参信息,并插入适当的类型转换。这意味着泛型不会比非泛型代码提供任何性能优势,因为它们移除了运行时强制转换,就像在 C#中一样。这也意味着泛型类型不能用于任何需要运行时信息的事情——比如创建泛型类型的新实例或者使用带有类型参数的instanceof操作符。允许的操作包括声明泛型类型的变量,将 null 赋给泛型变量,以及调用Object方法。
class MyClass<T>
{
public void myMethod(Object o)
{
T t1; // allowed
t1 = null; // allowed
System.out.print(t1.toString()); // allowed
if (o instanceof T) {} // invalid
T t2 = new T(); // invalid
}
}
从通用代码中移除类型信息的过程被称为类型擦除。例如,MyBox<Integer>将被简化为MyBox,这被称为原始类型。执行这一步是为了保持与泛型成为 Java 5 语言的一部分之前编写的代码的向后兼容性。
有界类型参数
可以对泛型可能使用的类型参数的种类应用编译时强制限制。这些限制称为边界,在类型参数部分使用extends关键字指定。类型参数可以由超类或接口限定。例如,下面的类B只能用一个类型参数来实例化,该类型参数要么是A类型,要么将该类作为超类。
// T must be or inherit from A
class B<T extends A> {}
class A {}
下一个示例指定一个接口作为绑定。这将把类型参数限制为仅实现指定接口或属于接口类型本身的那些类型。
// T must be or implement interface I
class C<T extends I> {}
interface I {}
通过在由&符号分隔的列表中指定多个界限,可以将多个界限应用于类型参数:
class D<T extends A & I> {}
与号代替逗号作为分隔符,因为逗号已经用于分隔类型参数:
class E<T extends A & I, U extends A & I> {}
除了将泛型的使用限制在特定的参数类型之外,应用边界的另一个原因是增加被边界类型支持的允许方法调用的数量。未绑定的类型只能调用Object方法。但是,通过应用超类或接口绑定,该类型的可访问成员也将变得可用。
class Fruit
{
public String name;
}
class FruitBox<T extends Fruit>
{
private T box;
public void FruitBox(T t) { box = t; }
public String getFruitName()
{
// Use of Fruit member allowed since T extends Fruit
return box.name;
}
}
泛型和对象
在 Java 5 中引入泛型之前,Object类型用于创建可以存储任何类型对象的容器类。随着泛型的出现,应该避免使用Object类型作为通用容器。这是因为编译器有助于确保泛型在编译时是类型安全的,这在使用Object类型时是做不到的。
Java 库中的集合类,包括ArrayList,都被替换成了通用版本。即便如此,任何泛型类仍然可以像非泛型类一样使用,只需省略类型参数部分。默认的Object类型将被用作类型参数。这就是为什么非通用版本的ArrayList仍然被允许。考虑非通用ArrayList的以下使用:
import java.util.ArrayList;
// ...
// Object ArrayList
ArrayList a = new ArrayList();
a.add("Hello World");
// ...
Integer b = (Integer)a.get(0); // run-time error
通过抛出一个ClassCastException,这种String到Integer的转换将在运行时失败。如果使用一个通用的ArrayList来代替,错误的转换将会在编译时被发现,或者立即在一个 IDE(比如 NetBeans)中被发现。与其他编码方法相比,这种编译时调试特性是使用泛型的一个主要优势。
import java.util.ArrayList;
// ...
// Generic ArrayList (recommended)
ArrayList<String> a = new ArrayList<>();
a.add("Hello World");
// ...
Integer b = (Integer)a.get(0); // compile-time error
使用泛型替代,只有指定的类型参数才被允许进入ArrayList集合。此外,从集合中获取的值不必强制转换为正确的类型,因为编译器会负责这一点。
二十四、Lambda 表达式
Java 8 引入了 lambda 表达式,它提供了一种使用表达式表示方法的简洁方式。lambda 表达式由三部分组成:参数列表、箭头操作符(->)和主体。下面的 lambda 采用两个整数参数并返回它们的和。
(int x, int y) -> { return x + y; };
通常不需要指定参数类型,因为编译器可以自动确定这些类型。这种类型推断也适用于返回类型。如果正文只包含一条语句,可以省略花括号,然后返回语句的结果。
(x, y) -> x + y;
λ对象
lambda 表达式是一个函数接口的表示,它是一个定义单一抽象方法的接口。因此,只要它的函数方法具有匹配的签名,它就可以绑定到这种接口的对象。
interface Summable
{
public int combine(int a, int b);
}
public class MyApp
{
public static void main(String[] args) {
Summable s = (x, y) -> x + y;
s.combine(2, 3); // 5
}
Java 8 中添加的java.util.function包中定义了常用的函数接口。在这个例子中,可以使用BinaryOperator<T>接口。它表示一个方法,该方法采用两个参数并返回与参数类型相同的结果。它的功能方法被命名为apply。
import java.util.function.*;
public class MyApp
{
public static void main(String[] args) {
BinaryOperator<Integer> adder = (x, y) -> x + y;
adder.apply(2, 3); // 5
}
}
当处理单个操作数并返回相同类型的值时,可以使用UnaryOperator函数接口。注意,当只有一个参数时,参数周围的括号可以省略。
UnaryOperator<Integer> doubler = x -> x*2;
doubler.apply(2); // 4
λ参数
与方法不同,lambda 表达式不属于任何类。它们本身就是对象,因为它们是函数接口的实例。这样做的好处是,它们提供了一种方便的方式将功能作为参数传递给另一个方法。在下面的例子中,使用了Runnable接口,它有一个不带参数也不返回值的函数方法。这个接口属于java.lang,它的抽象方法被命名为run。
public class MyApp
{
static void starter(Runnable s) { s.run(); }
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello");
starter(r); // "Hello"
}
}
你也可以通过定义一个匿名的内部类来实现这个功能,但是这种方法比 lambda 表达式要冗长得多。
Runnable r = new Runnable() {
@Override public void run() {
System.out.println("Hello");
}
};
starter(r); // "Hello"
lambda 表达式可以从其上下文中捕获变量,前提是引用的变量是 final 或有效的 final(仅赋值一次)。在下一个例子中,使用了Consumer函数接口,它表示一个接受一个参数并且不返回值的函数。
import java.util.function.*;
public class MyApp
{
final static String GREETING = "Hi ";
public static void main(String[] args) {
Consumer<String> c = (s) ->
System.out.println(GREETING + s);
c.accept("John"); // "Hi John"
}
}
在幕后,编译器将实例化一个包含单个方法的匿名类来表示一个 lambda 表达式。这使得 lambdas 能够完全向后兼容早期版本的 Java 运行时环境。