Java5新特性
泛型,也称为参数化类型,是很多语言支持的编程范式——在定义类型、接口或者方法时将类型作为参数。Java诞生以来最重大的语法改变当属泛型,它是Java5最重要的特性。Java泛型与C++的模板机制在形式上有些类似,但是Java泛型的实现方式——类型擦除导致二者有着根本性的不同。
使用泛型,可以获得以下好处:
- 在编译期获得更好的类型检查
- 消除不必要的强制转型代码
- 允许编程人员实现泛型算法
使用泛型机制设计的类,最常见的是容器类,Java5的集合框架大量使用了泛型机制(类似于C++的STL库):
1 2 3 4 5 |
public interface Collection<E>{}; public interface Set<E>{}; public interface List<E>{}; public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}; public interface Map<K,V> {} |
但是,任何字段、方法参数、方法返回值需要参数化的类型,都可以从泛型机制获益,例如下面这个通用的回调接口:
1 2 3 |
interface Callback<P,R>{ R invoke(P p); } |
所谓泛型类型( generic type),是指通过类型参数化的类/接口,这种类型的声明语法为:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ClassName<T1,...Tn>{ // T1...Tn是将ClassName参数化的类型形参(type parameter),或者叫类型变量(type variables),可以有1-N个 } // 示例: class Container<I> { private I item; void put(I i) { item = i; } I get() { return item; } } |
类型形参可以在类定义的任何地方引用。类型参数只是一个占位符,它可以表示任意一种非基本(non-primitive )类型。根据惯例,类型形参使用单个大写字母表示,一般E表示“元素”、K表示“键”、N表示“数字”、T表示“类型”、V表示“值”,等等。
要在代码中使用一个泛型类型,必须进行泛型类型调用(generic type invocation),此时必须把类型形参(type parameter)替换为类型实参( type argument),后者是真实的Java类型:
1 2 3 |
Container<Number> cn = new Container<Number>(); // Java 7引入了钻石操作符,可以根据cn的变量类型推倒构造器的type argument: Container<Number> cn = new Container<>(); |
作为类型实参的Java类型,其本身也可以是泛型类型:
1 2 3 4 |
// 不管嵌套多少层,泛型类型的类型参数都不能是占位符 Container<List<String>> cn = new Container<>(); // 下面的语句非法,E不是可解析的真实类型 Container<List<E>> cn = new Container<>(); |
所谓原始类型,是指在不指定类型实参的情况下使用泛型,例如: ArrayList list = new ArrayList() 。原始类型为依赖的Java库提供的向后兼容。
混合使用泛型、原始类型,会在编译时得到未检查错误信息(Unchecked Error Messages),所谓Unchecked表示编译器无法得到足够的信息来完成类型推断以确保安全性。要禁止此类错误信息,可以使用编译选项 -Xlint:-unchecked 或者在代码上添加注解 @SuppressWarnings("unchecked")
不但类可以设计为泛型,方法也可以。方法可以使用类声明的泛型形参,亦可自己声明泛型参数,声明自己的泛型参数的方法称为泛型方法(Generic method),例如:
1 2 3 4 5 6 |
public class Math { // 泛型方法,泛型形参列表必须在返回值之前声明: public static <RET, ARG0, ARG1> RET plus( ARG0 arg0, ARG1 arg1 ) { return (RET) null; } } |
调用泛型方法的完整语法如下:
1 |
Integer result = Math.<Integer, Double, Double>plus( 1.1, 1.2 ); |
指定泛型实参列表 不是必须的,通常编译器能够根据上下文推断。编译器的这种能力称为type inference。
Java泛型支持限制可作为泛型实参的类型的范围。例如上面的plus方法,可能仅仅支持数字类型。要声明受限泛型形参,需要使用extends关键字:
1 2 3 |
public static <RET, ARG0 extends Number, ARG1 extends Number> RET plus( ARG0 arg0, ARG1 arg1 ) { return (RET) null; } |
使用受限形参,方法体可以对形参的类型进行假设,这样就可以执行有意义的方法调用了:
1 2 3 4 5 6 7 8 |
public static <T extends Comparable<T>> int countGreaterThan( T[] anArray, T elem ) { int count = 0; for ( T e : anArray ) // 调用比较方法,因为T必须是Comparable的子类型 if ( e.compareTo( elem ) > 0 ) ++count; return count; } |
可以在限制泛型实参必须是某个类型或者其子类型的同时,限制它必须实现某些接口:
1 2 |
// 多个接口用 & 符号分隔 public static <T extends Entity & Serializable> void save(T t){} |
可以在字段、方法形参、变量的声明中使用(不受限的)泛型通配符,使用 ? 代替实际类型即为泛型通配符,表示泛型实参的类型未知。使用通配符后:
- 泛型类中返回参数类型的方法,类型自动变为Object
- 泛型类中需要参数类型作为入参的方法,将不能接受任何对象
例如泛型类:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Value<T> { private T t; public Value( T t ){ this.t = t; } public void set( T t ){ this.t = t; } public T get(){ return t; } } |
如果声明参数类型为通配符:
1 2 3 |
Value<?> value = new Value<Integer>( 1 ); Object o = value.get(); //可以通过编译,你不知道是什么类型,所以我们给你一个根类型 value.set( 1 ); //无法通过编译,不是我们不限制类型,而是你不知道是什么类型 |
就意味着,你可以从泛型对获取参数类型对象,但是却不能提供了。
Java 5还支持受限泛型通配符的声明,所谓受限是指,变量/返回值/参数的泛型参数必须满足限制:
- Type<? extends PType> :上限通配符(Upper Bounded Wildcard),表示变量的类型参数必须是PType或者PType的子类型
- Type<? super PType> :下限通配符(Lower Bounded Wildcard),表示变量的类型参数必须是PType或者PType的超类型
在设计一个接收泛型入参的接口时,如果你需要向入参提供一个对象,可以使用下限通配符,因为这可以确保你提供的对象可以被接受:
1 2 3 4 5 6 |
public static void addNumbers( List<? super Integer> list ) { // list的实参可以是Integer,或者Number,或者Object,但是必然能容纳int类型 for ( int i = 1; i <= 10; i++ ) { list.add( i ); } } |
在设计一个接收泛型入参的接口时,如果你需要使用入参提供的对象,可以使用上限通配符,这样你可以假设对象的类型:
1 2 3 4 5 6 |
public static void process( List<? extends Number> list ) { // list的实参可以是Integer,或者Number,或者Double,但是都可以针对其调用Number的接口 for ( Number n : list ) { n.intValue(); } } |
上面的两条,就是所谓的PECS原则:当泛型对象作为生产者(对外提供对象)时,将其泛型形参声明为上限通配符(extends);当泛型对象作为消费者时,将其泛型形参声明为下限通配符(super) ,下面的例子有助于进一步理解PECS:
1 2 3 4 5 6 7 8 9 10 11 |
Value<? extends Number> valueExtend = new Value<Integer>( 1 ); //有了这个变量声明,我知道类型参数必然是Number的子类型: Number num = valueExtend.get(); //但是,具体是Integer,还是Double,我不知道,因此我无法提供参数类型的实例 valueExtend.set( 1 ); //无法编译 Value<? super Number> valueSuper = new Value<Number>( 1 ); //有了这个变量声明,我知道类型参数必然是Number的超类型: valueSuper.set( new Integer( 1 ) ); //那么,我当然可以提供Number的子类型给你 //但是,我却不知道类型参数的真实类型 Object obj = valueSuper.get(); //所以此变量提供的参数类型未知 |
1 2 3 4 |
class BoundedBuffer<T> { private T[] items; } |
但是,就像不能实例化参数类型一样,你也不能实例化参数类型的数组:
1 2 |
T t = new T(); //无法通过编译,无法提供数组需要的运行时信息 T[] ts = new T[10]; //无法通过编译,编译器甚至无法确认T类型有默认构造器 |
不能实例化的原因是,Java中的数组在运行时必须知道其元素的类型。上述语法无法提供这一类型信息——因为T是什么在编译时根本不知道。因此泛型数组如果需要在泛型内部初始化,必须进行变通:
1 |
T[] ts = (T[]) new Object[10]; |
注意绝大部分的参数化类型信息都在编译期间擦除了,除了继承自泛型类的子类型,可以通过反射得到父类上的真实参数类型:
1 2 3 4 5 6 7 8 9 |
//它的泛型父类的类型信息String不会被擦除 class StringList extends ArrayList<String> //和C++的模板特化有些类似 { private static final long serialVersionUID = 1L; } //可以使用下面的代码获得泛型父类的真实参数类型 ParameterizedType type = (ParameterizedType) new StringList().getClass().getGenericSuperclass(); assert ( type.getActualTypeArguments()[0] == String.class ); |
需要注意泛型是“非协变”的,参数类型不同的泛型变量不能相互转换,即使这些参数类型之间存在继承关系:
1 2 3 4 |
//数组是协变的 Object oa[] = new Integer[] {}; //泛型不是,尽管泛型实参Object和Number具有父子类关系。但是List<Object>、List<Number>之类不存在父子类关系 List<Object> ol = new ArrayList<Number>(); //无法编译 |
使用上限通配符,可以解决非协变特性导致的无法赋值问题:
1 |
List<? extends Object> ol = new ArrayList<Number>(); |
继承泛型类型时,可以将全部或者部分泛型参数“特化”:
1 2 3 4 |
// 完全特化 abstract class StringList implements List<String> {} // 部分特化 abstract class StringKeyMap<V> implements Map<String,V> {} |
注意:上面的例子中,任何StringList是List的子类型,StringKeyMap是Map的子类型。在特化类型参数的同时,你可以获得重写父类逻辑的机会。
注解在Java5中第一次成为可以影响编程方式的语言元素,注解可以为类、字段、方法、甚至注解本身提供元数据信息。注解的出现将很大程度上改变可配置化对XML高度依赖的现状。
声明注解需要使用特殊的类型说明符: @interface ,例如:
1 2 3 4 5 6 7 8 9 10 11 |
//某些注解可以用来修饰注解类型本身 @Retention ( RetentionPolicy.RUNTIME ) //提示该注解需要在运行期保留 @Target ( ElementType.METHOD ) //提示该注解只能作用于方法上 @interface AutoExtAjaxResult { //注解中可以声明多个“方法”,用来指示注解实例的属性名 String value(); //value是一个特殊属性,如果注解只有这一个属性,在实例化注解时,可以不指定属性名 Class<?> type() default App.class; //注解属性可以提供默认值 } |
需要注意的是,注解属性只支持有限的类型:基本类型、字符串、注解、枚举、Class,或者这些类型的一维数组。
要使用(实例化)注解,只需要在目标类/方法/字段的声明前面添加@注解名,并提供必要的参数,所有没有提供默认值的属性都需要提供值:
1 2 3 4 5 |
@AutoExtAjaxResult ( "Avalue" ) public String userInfo() { return null; } |
要在运行时获取某个类/方法/字段上被附加的注解,需要使用反射机制,例如:
1 |
App.class.getMethod( "userInfo" ).getAnnotation( AutoExtAjaxResult.class ).value(); // Avalue |
数组以及任何实现了 java.lang.Iterable 接口的对象都可以使用该语法:
1 2 3 4 5 6 |
for ( char c : new char[] { 'H', 'E', 'L', 'L', 'O' } ) { System.out.println( c ); } for ( String str : new ArrayList() ) { System.out.println( str ); } |
现在基本类型与其包装类型的转换,可以自动进行了:
1 2 3 4 5 6 |
Number n = 1L; Double d = 1.1d; Boolean b = false; boolean bp = Boolean.TRUE; double dp = Double.valueOf( 1.1d ); long lp = new Long( 1l ); |
Java5中,枚举被作为一种特殊的类,可以使用enum关键字来声明,枚举类型不支持创建新实例。
枚举类型和普通类一样可以有字段、方法、构造器等部分,特别的是,枚举实例可以对方法进行覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public enum Color { RED( "FF0000" ), GREEN( "00FF00" ), BLUE( "0000FF" ) { //每个枚举值可以覆盖方法 public String toString() { return "00F"; } }; //和普通类一样,枚举可以具有字段、方法、构造器 private String code; private Color( String code ) { this.code = code; } @Override public String toString() { return code; } } |
特殊的语法可以声明方法接受不定长度的参数,变长参数的声明必须位于形参列表的尾部:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void main( String[] args ) { varargsMethod( 1, 2, "1", "2", "3" ); varargsMethod0( "1", "2", "3" ); } private static void varargsMethod0( String... strs ) { } private static void varargsMethod( int i, int j, String... strs ) { } |
可以使用import static语法导入某些类中的静态方法,在当前文件中不需要提供这些类的名字就可以直接调用静态方法:
1 2 3 4 5 6 |
import static java.util.Collections.*; public static void main( String[] args ) { sort( list ); //不需要提供Collections类名 } |
这是一个Java5引入的专门用于并发编程的新包,该包主要有以下组件:
- 支持并发的集合类型,位于java.util.concurrent包
- 原子操作类,这些原子操作类受益于JVM引入了CAS(Compare-And-Set)机制,位于java.util.concurrent.atomic包
- 新的同步原语,主要是一些锁类,位于java.util.concurrent.locks包
- 并发相关工具,例如线程池、信号量等
- 异步相关类,例如Future、Executor
concurrent中的集合类图如下(深色表示JDK 1.6追加):
可以看到类很多,因此不一一描述,仅将类名中单词的含义列在下表:
单词 | 含义 |
Queue | 提供先进先出的操作接口 |
Deque | 不但支持在列表的尾部操作,还支持在头部操作 |
Concurrent | 表示支持线程安全的并发访问 |
Linked | 表示链表结构 |
Array | 表示数组结构(连续内存,高速随机访问) |
Blocking | 当集合为空,取数据操作将阻塞;当集合为满,存数据操作将阻塞 |
Synchronous | 一个存操作的完成,必须依赖有一个取操作的执行 |
Priority | 允许元素被优先取出 |
Delay | 元素只有在超时后,才能从集合中取出 |
CopyOnWrite | 一旦执行写操作,就复制底层的数据结构(例如数组),复制的成本较高,但是CopyOnWrite避免了同步开销,适用于大量读、很少写的应用场景 |
Navigable | 可以导航,即根据给定的元素,寻找其最近的元素 |
concurrent.atomic包中的原子操作类包括:Boolean、Integer、Long、引用的原子类及其数组类型。
concurrent.locks包引入了一些用于支持锁定的类和接口,改变了Java只能依靠 synchronized 关键字进行粗粒度同步控制的现状:
该包主要有三个接口:
接口/类 | 说明 |
Lock |
用于代替 synchronized 关键字的功能,可以关联多个 Condition) ,一般锁都是独占的进行共享资源的访问。该接口提供了比 synchronized 更灵活的功能:
|
Condition |
用于代替 java.lang.Object 定义的监视器方法:wait、notify和notifyAll。该接口可以把前面几个监视器方法的职责分解为多个完全不同的“条件”对象,条件对象总是和一个锁关联。
|
ReadWriteLock |
维护一对相关的锁:一个用于只读操作,另外一个用于写入操作,规则如下:
|
和异步编程有关的接口主要有四个:
- Future接口表示一个“在未来某个时间点可用的结果”
- Callable表示一个可调用对象
- Executor接口可以用来执行一个Runnable
- CompletionService则可用来调用Callable并(异步)获得结果
这些类的层次结构如下:
concurrent包包含的其它工具类有:
类 | 说明 |
Semaphore | 信号量,线程可以释放信号量,亦可尝试获取信号量,信号量相当于维护了一个固定数量的许可 |
CountDownLatch | 计数器,可以让某个线程在计数完毕前等待。其它线程则可以降低计数值 |
CyclicBarrier | 屏障,允许多个线程相互等待(await),直到所有线程都调用了await(),所有线程才会继续运行 |
TimeUnit | 这是一个枚举,可以用来进行时间单位的转换,或者让当前线程睡眠一段时间 |
Leave a Reply