java基础—泛型

1、泛型入门

  Java集合有个缺点—把一个对象“丢进”集合里之后, 集合就会 “忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译 类型就变成了Object类型(其运行时类型没变)。
  Java集合之所以被设计成这样, 是因为集合的设计者不知道我们 会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何 类型的对象, 只要求具有很好的通用性。 但这样做带来如下两个问题:
  ➢ 集合对元素类型没有任何限制, 这样可能引发一些问题。 例 如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地 将Cat对象“丢”进去,所以可能引发异常。
  ➢ 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集 合只知道它盛装的是Object, 因此取出集合元素后通常还需要 进行强制类型转换。 这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

1.1、编译时不检查类型的异常

  下面程序将会看到编译时不检查类型所导致的异常。

public class ListErr {
    public static void main(String[] args) {
        // 创建一个只想保存字符串的List集合
        ArrayList strList = new ArrayList();
        strList.add("Java");
        strList.add("Android");
        // "不小心"把一个Integer对象"丢进"了集合
        strList.add(5);     // ①
        strList.forEach(str -> System.out.println(((String) str).length())); // ②
    }
}

  上面程序创建了一个List集合, 而且只希望该List集合保存字符 串对象—但程序不能进行任何限制,如果程序在①处“不小心”把一 个Integer对象“丢进”了List集合中, 这将导致程序在②处引发 ClassCastException异常, 因为程序试图把一个Integer对象转换为 String类型。

1.2、使用泛型

  从 Java 5 以 后 , Java 引 入 了 “ 参 数 化 类 型 ( parameterized type)”的概念, 允许程序在创建集合时指定集合元素的类型, 正如 在第8章的ShowHand.java程序中见到的List, 这表明该List 只 能 保 存 字 符 串 类 型 的 对 象 。 Java 的 参 数 化 类 型 被 称 为 泛 型 (Generic)。
  对于前面的ListErr.java程序,可以使用泛型改进这个程序。

public class GenericList {
    public static void main(String[] args) {
        // 创建一个只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();  // ①
        strList.add("Java");
        strList.add("Android");
        // 下面代码将引起编译错误
		strList.add(5);    // ②
        strList.forEach(str -> System.out.println(str.length())); // ③
    }
}

  上面程序将在②处引发编译异常, 因为strList集合只能添加 String对象,所以不能将Integer对象“丢进”该集合。
  而且程序在③处不需要进行强制类型转换,因为strList对象可以 “记住”它的所有集合元素都是String类型。
  上面代码不仅更加健壮, 程序再也不能“不小心”地把其他对象 “丢进”strList集合中;而且程序更加简洁,集合自动记住所有集合 元素的数据类型,从而无须对集合元素进行强制类型转换。

1.3、“菱形”语法

  在Java 7以前, 如果使用带泛型的接口、类定义变量, 那么调用 构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。 例如如下两条语句:

        Map<String, Object> map = new HashMap<String, Object>();
        List<String> strList = new ArrayList<String>();  

  上面两条语句中"<>"里的代码部分完全是多余的, 在Java 7以前这是必需的, 不能省略。 从Java 7开始, Java允许在构造器后不需要 带完整的泛型信息, 只要给出一对尖括号(<>)即可, Java可以推断 尖括号里应该是什么泛型信息。 即上面两条语句可以改写为如下形式:

        Map<String, Object> map = new HashMap<>();
        List<String> strList = new ArrayList<>();  

  把两个尖括号并排放在一起非常像一个菱形, 这种语法也就被称 为“菱形”语法。下面程序示范了Java 7及以后版本的菱形语法。

public class DiamondTest {
    public static void main(String[] args) {
        // Java自动推断出ArrayList的<>里应该是String
        List<String> books = new ArrayList<>();
        books.add("Java");
        books.add("Android");
        // 遍历books集合,集合元素就是String类型
        books.forEach(ele -> System.out.println(ele.length()));
        // Java自动推断出HashMap的<>里应该是String, List<String>
        Map<String, List<String>> schoolsInfo = new HashMap<>();
        // Java自动推断出ArrayList的<>里应该是String
        List<String> schools = new ArrayList<>();
        schools.add("斜月三星洞");
        schools.add("西天取经路");
        schoolsInfo.put("孙悟空", schools);
        // 遍历Map时,Map的key是String类型,value是List<String>类型
        schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value));
    }
}

  Java 9再次增强了“菱形”语法, 它甚至允许在创建匿名内部类 时使用菱形语法, Java可根据上下文来推断匿名内部类中泛型的类 型。下面程序示范了在匿名内部类中使用菱形语法。

interface Foo<T> {
    void test(T t);
}

public class AnnoymousDiamond {
    public static void main(String[] args) {
        // 指定Foo类中泛型为String
        Foo<String> f = new Foo<>() {
            // ①test()方法的参数类型为String
            public void test(String t) {
                System.out.println("test方法的t参数为:" + t);
            }
        };
        // ②使用泛型通配符,此时相当于通配符的上限为Object
        Foo<?> fo = new Foo<>() {
            // test()方法的参数类型为Object
            public void test(Object t) {
                System.out.println("test方法的Object参数为:" + t);
            }
        };
        // ③使用泛型通配符,通配符的上限为Number
        Foo<? extends Number> fn = new Foo<>() {
            // 此时test()方法的参数类型为Number
            public void test(Number t) {
                System.out.println("test方法的Number参数为:" + t);
            }
        };
    }
}

  上面程序先定义了一个带泛型声明的接口, 接下来123处码分别示范了在匿名内部类中使用菱形语法。第1处代码声明变量时明确地将泛型指定为String类型,因此在该匿名内部类中T类型就代表了String类型;第2处字代码声明变量时使用通配符来代表泛型(相当于通配符的上限为Object),因此系统只能推断出T代表 Object,所以在该匿名内部类中T类型就代表了Object类型;第3处代码声明变量时使用了带上限(上限是Number)的通配符, 因此系统可以推断出T代表Number类。

2、深入泛型

2.1、定义泛型接口、类

  可以为任何类、接口增加泛型声明(并不是只有集合类才可以使 用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个 Apple类,这个Apple类就可以包含一个泛型声明。

public class Apple<T> {
    // 使用T类型定义实例变量
    private T info;

    public Apple() {
    }

    // 下面方法中使用T类型来定义构造器
    public Apple(T info) {
        this.info = info;
    }

    public void setInfo(T info) {
        this.info = info;
    }

    public T getInfo() {
        return this.info;
    }

    public static void main(String[] args) {
        // 由于传给T形参的是String,所以构造器参数只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());
        // 由于传给T形参的是Double,所以构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}

  上面程序定义了一个带泛型声明的Apple类(不要理会这个泛 型形参是否具有实际意义),使用Apple类时就可为T形参传入实际类型, 这样就可以生成如Apple、Apple…形式的多 个 逻 辑 子 类 ( 物 理 上 并 不 存 在 ) 。 这 就 是 可 以 使 用 List、ArrayList等类型的原因—JDK在定义List、 ArrayList等接口、类时使用了泛型声明,所以在使用这些类时为之传入了实际的类型参数。
  当创建带泛型声明的自定义类,为该类定义构造器时,构造器 名还是原来的类名, 不要增加泛型声明。 例如, 为Apple类定义 构造器,其构造器名依然是Apple,而不是Apple!调用该构造器 时却可以使用Apple的形式,当然应该为T形参传入实际的类型参 数。Java 7提供了“菱形”语法,允许省略<>中的类型实参。

2.2、从泛型类派生子类

  当创建了带泛型声明的接口、父类之后, 可以为该接口创建实现 类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时 不能再包含泛型形参。例如,下面代码就是错误的。

//定义类A继承Apple类, Apple类不能跟泛型形参 
public class A extends Apple<T>{}

  如果想从Apple类派生一个子类,则可以改为如下代码:

// 使用Apple类时为T形参传入 String类型
public class A extends Apple<String>{}

  调用方法时必须为所有的数据形参传入参数值, 与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。

// 使用Apple类时,没有为形参传入实际的类型参数
public class A extends Apple{}

  像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。
  如果从Apple类派生子类,则在Apple类中所有使用T类型 的地方都将被替换成String类型, 即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法, 如果子类需要重 写父类的方法,就必须注意这一点。下面程序示范了这一点。

public class A1 extends Apple<String> {
    // 正确重写了父类的方法,返回值
    // 与父类Apple<String>的返回值完全相同
    public String getInfo() {
        return "子类" + super.getInfo();
    }
	
	// 下面方法是错误的,重写父类方法时返回值类型不一致
	public Object getInfo() {
		return "子类";
	}
}

2.3、并不存在泛型类

  前面提到可以把ArrayList类当成ArrayList的子类, 事 实上, ArrayList类也确实像一种特殊的ArrayList类:该 ArrayList对象只能添加String对象作为集合元素。 但实际 上, 系统并没有为ArrayList生成新的class文件, 而且也不 会把ArrayList当成新类来处理。
  看下面代码的打印结果是什么?

//分別创建Iist<String>对象和List<Integer>对象
List<String> l1 =new ArrayList<>();
List<Integer> l2 new ArrayList<>();
//调用 getclass()方法来比较l1和l2的类是否相等
System.out.println(l1.getclass() == l2.getclass());

  运行上面的代码片段,可能有读者认为应该输出false,但实际输 出true。 因为不管泛型的实际类型参数是什么, 它们在运行时总有同 样的类(class)。
  不管为泛型形参传入哪一种类型实参, 对于Java来说, 它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。下面程序演示了这种错误。

public class R<T> {
    // 下面代码错误,不能在静态变量声明中使用泛型形参
	static T info;
    T age;

    public void foo(T msg) {
    }
    
    // 下面代码错误,不能在静态方法声明中使用泛型形参
	public static void bar(T msg){}

}

  由于系统中并不会真正生成泛型类, 所以instanceof运算符后不 能使用泛型类。例如,下面代码是错误的。

java.util.Collection<String> cs = new java.util.ArrayList<>();
// 下面代码编译时引起错误: instanceof运算符后不能使用泛型
if(cs instanceof java.util.Arraylist<string>) {
}

3、类型通配符

  当使用一个泛型类时(包括声明变量和创建对 象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传 入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个 方法,该方法里有一个集合形参,集合形参的元素类型是不确定的, 那应该怎样定义呢?
  例如如下程序实例。

public void test(list c) {
	for (var i=0: i<c.size(): i++) {
		System.out.printin(c.get(i));
	}
}

  上面程序当然没有问题:这是一段最普通的遍历List集合的代 码。 问题是上面程序中List是一个有泛型声明的接口, 此处使用List 接口时没有传入实际类型参数, 这将引起泛型警告。 为此, 考虑为 List接口传入实际的类型参数—因为List集合里的元素类型是不确定 的,将上面方法改为如下形式:

public void test(list<Object> c) {
	for (var i=0: i<c.size(): i++) {
		System.out.printin(c.get(i));
	}
}

  表面上看起来, 上面方法声明没有问题, 这个方法声明确实没有 任何问题。问题是调用该方法传入的实际参数值时可能不是我们所期 望的,例如,下面代码试图调用该方法。

List<string> strList=new ArrayList<>();
//将 stylist作为参数来调用前面的test方法 
test(styList);//①

  编译上面程序,将在①处发生编译错误,提示List对象不能被当成 List 对 象 使 用 , 也 就 是 说 , List 类 并 不 是 List类的子类。
  注意点:如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛 型声明的类或接口, G并不是G的子类型!这一点非常值 得注意,因为它与大部分人的习惯认为是不同的。这也是导致上面异常的原因。

3.1、使用类型通配符

  为了表示各种泛型List的父类, 可以使用类型通配符, 类型通配 符是一个问号(?),将一个问号作为类型实参传给List集合,写作: List<?>(意思是元素类型未知的List)。 这个问号(? )被称为通配 符,它的元素类型可以匹配任何类型。可以将上面方法改写为如下形式:

public void test(List<?> c) {
	for (int i=0; i<c.size(); i++) {
		System.out.printin(c.get(i));
	}
}

  现在使用任何类型的List来调用它,程序依然可以访问集合c中的 元素, 其类型是Object, 这永远是安全的, 因为不管List的真实类型 是什么,它包含的都是Object。
  但这种带通配符的List仅表示它是各种泛型List的父类, 并不能 把元素加入到其中。例如,如下代码将会引起编译错误。

List<?> c =new ArrayList<String>();
//下面程序引起编译错误
c.add(new Object());

  因为程序无法确定c集合中元素的类型, 所以不能向其中添加对 象。 根据前面的List接口定义的代码可以发现:add()方法有类型 参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者 其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将 任何对象“丢进”该集合。 唯一的例外是null, 它是所有引用类型的 实例。

3.2、设定类型通配符的上限

  当直接使用List<?>这种形式时,即表明这个List集合可以是任何 泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是 任何泛型List的父类, 只希望它代表某一类泛型List的父类。 考虑一个简单的绘图程序,下面先定义三个形状类。

// 定义一个抽象类Shape
public abstract class Shape {
    public abstract void draw(Canvas c);
}


// 定义Shape的子类Circle
public class Circle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画一个圆");
    }
}

// 定义Shape的子类Rectangle
public class Rectangle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("把一个矩形画在画布" + c + "上");
    }
}

  上面定义了三个形状类,其中Shape是一个抽象父类,该抽象父类 有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布 类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个 Canvas类呢?考虑如下的Canvas实现类。

public class Canvas {
	// 同时在画布上绘制多个形状
	public void drawAll(List<Shape> shapes){
		for (Shape s : shapes){
			s.draw(this);
		}
	}
}

  注 意 上 面 的 drawAll() 方 法 的 形 参 类 型 是 List , 而 List 并不是List的子类型,这一点前面已经用示例说明过了,因此,下面代码将引起编 译错误。

	List<Circle> circleList = new ArrayList<>();
	Canvas c= new Canvas();
	//不能把List<Circle>当成List<Shape>使用,所以下面代码引起编译错误
	c.drawall(circleList);

  关键在于List 并不是List的子类型,所以不能把 List 对象当成List使用。为了表示List 的父类, 可以考虑使用List<?>, 但此时从List<?>集合中取出的元素只能被编译器当成Object处理。为了表示List集合的所有元素是Shape的子类, Java泛型提供了被限制的泛型通配符。 被限制的泛型通配符表示如下:

// 它表示泛型形参必须是Shape子类的List
List<? extends Shape>

  有了这种被限制的泛型通配符, 就可以把上面的Canvas程序改为 如下形式(程序清单同上):

public class Canvas {
    // 同时在画布上绘制多个形状,使用被限制的泛型通配符
    public void drawAll(List<? extends Shape> shapes) {
        for (Shape s : shapes) {
            s.draw(this);
        }
    }
}

  将Canvas改为如上形式, 就可以把List 对象当成List<? extends Shape> 使 用 。 即 List<? extends Shape> 可 以 表 示 List 、List的父类—只要List后尖括号里的类型 是Shape的子类型即可。
  List<? extends Shape>是受限制通配符的例子, 此处的问号 (?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的 这个未知类型一定是Shape的子类型(也可以是Shape本身), 因此可 以把Shape称为这个通配符的上限(upper bound)。
  同样,由于程序无法确定这个受限制的通配符的具体类型,所以还是不能把Shape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。

public void addrectangle(List<? extends Shape> shapes) {
	//下面代码引起编译错误
	shapes.add(0, new Rectangle()),
}

  与使用普通通配符相似的是, shapes.add()的第二个参数类型 是?extends Shape, 它表示Shape未知的子类, 程序无法确定这个类 型是什么,所以无法将任何对象添加到这种集合中。
  简而言之, 这种指定通配符上限的集合, 只能从集合中取元素 (取出的元素总是上限的类型或其子类), 不能向集合中添加元素 (因为编译器没法确定集合元素实际是哪种子类型)。
  对于更广泛的泛型类来说, 指定通配符上限就是为了支持类型型 变。 比如Foo是Bar的子类, 这样A就相当于A<? extends Bar>的 子类, 可以将A赋值给A<? extends Bar>类型的变量, 这种型变 方式被称为协变。
  对于协变的泛型而言, 它只能调用泛型类型作为返回值类型的方 法(编译器会将该方法返回值当成通配符上限的类型);而不能调用 泛型类型作为参数的方法。口诀是:协变只出不进!
  没有指定通配符上限的泛型类,相当于通配符上限是Object。

3.3、设定类型通配符的下限

  除可以指定通配符的上限之外, Java也允许指定通配符的下限, 通配符的下限用<? super类型>的方式来指定,通配符下限的作用与通 配符上限的作用恰好相反。
  指定通配符的下限就是为了支持类型型变。 比如Foo是Bar的子 类, 当程序需要一个A<? super Foo>变量时, 程序可以将A、 A赋值给A<? super Foo>类型的变量,这种型变方式被称为逆 变。
  对于逆变的泛型集合来说, 编译器只知道集合元素是下限的父类 型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向 其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从 集合中取元素时只能被当成Object类型处理(编译器无法确定取出的 到底是哪个父类的对象)。
  对于逆变的泛型而言, 它只能调用泛型类型作为参数的方法;而 不能调用泛型类型作为返回值类型的方法。口诀是:逆变只进不出!
  假设自己实现一个工具方法:实现将src集合中的元素复制到dest 集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest 集合元素的类型应该是src集合元素类型的父类。
  对于上面的copy()方法, 可以这样理解两个集合参数之间的依赖 关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前 者相同或者是前者的父类即可,此时通配符的下限就有了用武之地。 下面程序采用通配符下限的方式来实现该copy()方法。

public class MyUtils {
    // 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
    public static <T> T copy(List<? super T> dest, List<T> src) {
        T last = null;
        for (T ele : src) {
            last = ele;
            // 逆变的泛型集合添加元素是安全的
            dest.add(ele);
        }
        return last;
    }

    public static void main(String[] args) {
        // var声明的变量,后面必须具体指定泛型的类型
        List<Number> ln = new ArrayList<>();
        ArrayList<Integer> li = new ArrayList<>();
        li.add(5);
        // 此处可准确的知道最后一个被复制的元素是Integer类型
        // 与src集合元素的类型相同
        Integer last = copy(ln, li);    // ①
        System.out.println(ln);
    }
}

  使用这种语句, 就可以保证程序的①处调用后推断出最后一个被 复制的元素类型是Integer,而不是笼统的Number类型。

3.4、设定泛型形参的上限

  Java泛型不仅允许在使用通配符形参时设定上限, 而且可以在定 义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是 该上限类型,要么是该上限类型的子类。下面程序示范了这种用法。

public class Apple<T extends Number> {
    T col;

    public static void main(String[] args) {
        Apple<Integer> ai = new Apple<>();
        Apple<Double> ad = new Apple<>();
        // 下面代码将引起编译异常,下面代码试图把String类型传给T形参
        // 但String不是Number的子类型,所以引发编译错误
		Apple<String> as = new Apple<>();		// ①
    }
}

4、泛型方法

  前面介绍了在定义类、接口时可以使用泛型形参, 在该类的方法 定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普 通类型来用。 在另外一些情况下, 定义类、接口时没有使用泛型形 参, 但定义方法时想自己定义泛型形参, 这也是可以的, Java 5还提 供了对泛型方法的支持。

4.1、定义泛型方法

  假设需要实现这样一个方法—该方法负责将一个Object数组的所 有元素添加到一个Collection集合中。 考虑采用如下代码来实现该方法。

static void fromarraytocollection(Object[] a, Collection<Object> c) {
	for (Object o: a) {
		c.add(o);
	}
}

  上面定义的方法没有任何问题,关键在于方法中的c形参,它的数 据 类 型 是 Collection 。 正 如 前 面 所 介 绍 的 , Collection不是Collection的子类型—所以这个方 法的功能非常有限, 它只能将Object[]数组的元素复制到元素为 Object(Object的子类不行)的Collection集合中, 即下面代码将引起编译错误。

	String[] strArr = {"a", "pi"};
	List<String> strList = new ArrayList<>();
	// Collections<String>对象不能当成 Collection<Object>使用,下面代码出现编译错误 
	fromarraytocollection(strArr, strList);

  为了解决这个问题, 可以使用Java 5提供的泛型方法(Generic Method)。 所谓泛型方法,就是在声明方法时定义一个或多个泛型形参。泛型方法的语法格式如下:

修饰符<T, S> 返回值类型 方法名(形参列表) {
	// 方法体
}

  把上面方法的格式和普通方法的格式进行对比, 不难发现泛型方 法的方法签名比普通方法的方法签名多了泛型形参声明,泛型形参声 明以尖括号括起来,多个泛型形参之间以逗号(,)隔开,所有的泛型 形参声明放在方法修饰符和方法返回值类型之间。
  采用支持泛型的方法,就可以将上面的fromArrayToCollection方法改为如下形式:

static <T> void fromarraytocollection(T[] a, Collection<T> c) {
	for (T o: a) {
		c.add(o);
	}
}

  下面程序示范了完整的用法。

public class GenericMethodTest {
    // 声明一个泛型方法,该泛型方法中带一个T泛型形参,
    static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
        for (T o : a) {
            c.add(o);
        }
    }

    public static void main(String[] args) {
        Object[] oa = new Object[100];
        Collection<Object> co = new ArrayList<>();
        // 下面代码中T代表Object类型
        fromArrayToCollection(oa, co);
        String[] sa = new String[100];
        Collection<String> cs = new ArrayList<>();
        // 下面代码中T代表String类型
        fromArrayToCollection(sa, cs);
        // 下面代码中T代表Object类型
        fromArrayToCollection(sa, co);
        Integer[] ia = new Integer[100];
        Float[] fa = new Float[100];
        Number[] na = new Number[100];
        Collection<Number> cn = new ArrayList<>();
        // 下面代码中T代表Number类型
        fromArrayToCollection(ia, cn);
        // 下面代码中T代表Number类型
        fromArrayToCollection(fa, cn);
        // 下面代码中T代表Number类型
        fromArrayToCollection(na, cn);
        // 下面代码中T代表Object类型
        fromArrayToCollection(na, co);
        // 下面代码中T代表String类型,但na是一个Number数组,
        // 因为Number既不是String类型,
        // 也不是它的子类,所以出现编译错误
		fromArrayToCollection(na, cs);
    }
}

  上面程序定义了一个泛型方法,该泛型方法中定义了一个T泛型形 参,这个T类型就可以在该方法内当成普通类型使用。与接口、类声明 中定义的泛型不同的是, 方法声明中定义的泛型只能在该方法里使 用,而接口、类声明中定义的泛型则可以在整个接口、类中使用。
  与类、接口中使用泛型参数不同的是, 方法中的泛型参数无须显 式传入实际类型参数,如上面程序所示,当程序调用 fromArrayToCollection()方法时,无须在调用该方法前传入String、 Object等类型, 但系统依然可以知道为泛型实际传入的类型, 因为编 译器根据实参推断出泛型所代表的类型, 它通常推断出最直接的类型。例如,下面调用代码:

       fromArrayToCollection(sa, cs);

  上面代码中cs是一个Collection类型, 与方法定义时的 fromArrayToCollection(T[] a, Collectionc)进行比较—只比 较泛型参数,不难发现该T类型代表的实际类型是String类型。
  为了让编译器能准确地推断出泛型方法中泛型的类型,不要制造迷惑!看如下程序。

public class ErrorTest {
    // 声明一个泛型方法,该泛型方法中带一个T泛型形参
    static <T> void test(Collection<T> from, Collection<T> to) {
        for (T ele : from) {
            to.add(ele);
        }
    }

    public static void main(String[] args) {
        List<Object> as = new ArrayList<>();
        List<String> ao = new ArrayList<>();
        // 下面代码将产生编译错误
        test(as, ao);
    }
}

  上面程序中定义了test()方法, 该方法用于将前一个集合里的元 素复制到下一个集合中, 该方法中的两个形参from、to的类型都是 Collection,这要求调用该方法时的两个集合实参中的泛型类型相 同,否则编译器无法准确地推断出泛型方法中泛型形参的类型。
  上面程序中调用test方法传入了两个实际参数, 其中as的数据类 型是List, 而ao的数据类型是List, 与泛型方法签 名进行对比:test(Collection a, Collection c), 编译器无 法正确识别T所代表的实际类型。为了避免这种错误,可以将该方法改为如下形式:

public class RightTest {
    // 声明一个泛型方法,该泛型方法中带一个T形参
    static <T> void test(Collection<? extends T> from, Collection<T> to) {
        for (T ele : from) {
            to.add(ele);
        }
    }

    public static void main(String[] args) {
        List<Object> ao = new ArrayList<>();
        List<String> as = new ArrayList<>();
        // 下面代码完全正常
        test(as, ao);
    }
}

  上面代码改变了test()方法签名, 将该方法的前一个形参类型改 为Collection<? extends T>,这种采用类型通配符的表示方式,只要 test() 方 法 的 前 一 个 Collection 集 合 里 的 元 素 类 型 是 后 一 个 Collection集合里元素类型的子类即可。

4.2、泛型方法和类型通配符的区别

  大多数时候都可以使用泛型方法来代替类型通配符。 例如, 对于 Java的Collection接口中两个方法定义:

public interface Collection<E> extends Iterable<E> {
    boolean containsAll(Collection<?> var1);
    boolean addAll(Collection<? extends E> var1);
}

  上面集合中两个方法的形参都采用了类型通配符的形式, 也可以 采用泛型方法的形式,如下所示。

public interface Collection<E> extends Iterable<E> {
    <T> boolean containsAll(Collection<T> var1);
    <T extends E> boolean addAll(Collection<T> var1);
}

  上面方法使用了泛型形式,这时定义泛型形参时设 定上限(其中E是Collection接口里定义的泛型, 在该接口里E可当成 普通类型使用)。
  上面两个方法中泛型形参T只使用了一次, 泛型形参T产生的唯一 效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应 该使用通配符:通配符就是被设计用来支持灵活的子类化的。
  泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的 类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没 有这样的类型依赖关系,就不应该使用泛型方法。
  例如某个方法中一个形参(a)的类型或返回值的类型依赖于另 一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符 —因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果 形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在 这种情况下,只能考虑使用在方法签名中声明泛型,也就是泛型方法。
  如果有需要,也可以同时使用泛型方法和通配符, 如Java的 Collections.copy()方法。

public class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {...}
}

  上面copy方法中的dest和src存在明显的依赖关系,从源List中复制出来的元素, 必须可以“丢进”目标List中, 所以源List集合元素 的类型只能是目标集合元素的类型的子类型或者它本身。 但JDK定义 src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该 方法无须向src集合中添加元素, 也无须修改src集合里的元素, 所以 可以使用类型通配符,无须使用泛型方法。
  简而言之,指定上限的类型通配符支持协变,因此这种协变的 集合可以安全地取出元素(协变只出不进),因此无须使用泛型法。
  当然,也可以将上面的方法签名改为使用泛型方法,不使用类型 通配符,如下所示。

public class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {...}
}

  这个方法签名可以代替前面的方法签名。 但注意上面的泛型形参 S,它仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于 它,那泛型形参S就没有存在的必要,即可以用通配符来代替S。使用 通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰 和准确。
  类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型, 也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方 法中显式声明。

4.3、“菱形”语法与泛型构造器

  正如泛型方法允许在方法签名中声明泛型形参一样, Java也允许 在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。
  一旦定义了泛型构造器, 接下来在调用构造器时, 就不仅可以让 Java根据数据参数的类型来“推断”泛型形参的类型, 而且也可以显式地为构造器中的泛型形参指定实际的类型。如下程序所示。

class Foo {
    public <T> Foo(T t) {
        System.out.println(t);
    }
}

public class GenericConstructor {
    public static void main(String[] args) {
        // 泛型构造器中的T类型为String。
        new Foo("疯狂Java讲义");
        // 泛型构造器中的T类型为Integer。
        new Foo(200);
        // 显式指定泛型构造器中的T类型为String,
        // 传给Foo构造器的实参也是String对象,完全正确。
        new <String>Foo("疯狂Android讲义");
        // 显式指定泛型构造器中的T类型为String,
        // 但传给Foo构造器的实参是Double对象,下面代码出错
		new <String> Foo(12.3);
    }
}

  前面介绍过Java 7新增的“菱形”语法, 它允许调用构造器时在 构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛 型构造器中声明的泛型形参的实际类型, 则不可以使用“菱形”语 法。如下程序所示。

class MyClass<E> {
    public <T> MyClass(T t) {
        System.out.println("t参数的值为:" + t);
    }
}

public class GenericDiamondTest {
    public static void main(String[] args) {
        // MyClass类声明中的E形参是String类型。
        // 泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc1 = new MyClass<>(5);
        // 显式指定泛型构造器中声明的T形参是Integer类型,
        MyClass<String> mc2 = new <Integer>MyClass<String>(5);
        // MyClass类声明中的E形参是String类型。
        // 如果显式指定泛型构造器中声明的T形参是Integer类型
        // 此时就不能使用"菱形"语法,下面代码是错的。
		MyClass<String> mc3 = new <Integer> MyClass<>(5);
    }
}

4.4、泛型方法与方法重载

  因为泛型既允许设定通配符的上限, 也允许设定通配符的下限, 从而允许在一个类里包含如下两个方法定义。

public class MyUtils {
    public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {...};
    public static <T> T copy(Collection<? super T> dest, Collection<T> src) {...}

  上面的MyUtils类中包含两个copy()方法,这两个方法的参数列表 存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都 是Collection对象, 前一个集合里的集合元素类型是后一个集合里集 合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错 误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

List<Number> ln =new ArrayList>();
List<Integer> li =new ArrayList>();
Myutils.copy(ln, li);

  上面程序第三行代码调用copy()方法, 但这个copy()方法既可以匹配①号copy()方法,此时泛型T表示的类型是Number;也可以匹配 ②号copy()方法, 此时泛型T表示的类型是Integer。 编译器无法确定 这行代码想调用哪个copy()方法,所以这行代码将引起编译错误。

4.5、类型推断

  Java 8改进了泛型方法的类型推断能力, 类型推断主要有如下两 方面。 ➢ 可通过调用方法的上下文来推断泛型的目标类型。 ➢ 可在方法调用链中,将推断得到的泛型传递到最后一个方法。

class MyUtil<E> {
    public static <Z> MyUtil<Z> nil() {
        return null;
    }

    public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) {
        return null;
    }

    E head() {
        return null;
    }
}

public class InferenceTest {
    public static void main(String[] args) {
        // 可以通过方法赋值的目标参数来推断类型参数为String
        MyUtil<String> ls = MyUtil.nil();
        // 无需使用下面语句在调用nil()方法时指定类型参数的类型
        MyUtil<String> mu = MyUtil.<String>nil();
        // 可调用cons方法所需的参数类型来推断类型参数为Integer
        MyUtil.cons(42, MyUtil.nil());
        // 无需使用下面语句在调用nil()方法时指定类型参数的类型
        MyUtil.cons(42, MyUtil.<Integer>nil());

        // 希望系统能推断出调用nil()方法类型参数为String类型,
        // 但实际上Java 8依然推断不出来,所以下面代码报错
		String s = MyUtil.nil().head();
		// 上面这行代码必须显式指定泛型的实际类型, 即将代码改 为如下形式
        String s = MyUtil.<String>nil().head();
    }
}

5、擦除和转换

  在严格的泛型代码里, 带泛型声明的类总应该带着类型参数。 但 为了与老的Java代码保持一致, 也允许在使用带泛型声明的类时不指 定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作 raw type(原始类型), 默认是声明该泛型形参时指定的第一个上限 类型。
  当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量 时,所有在尖括号之间的类型信息都将被扔掉。比如一个 List类型被转换为List, 则该List对集合元素的类型检查变 成了泛型参数的上限(即Object)。下面程序示范了这种擦除。

class Apple<T extends Number> {
    T size;

    public Apple() {
    }

    public Apple(T size) {
        this.size = size;
    }

    public void setSize(T size) {
        this.size = size;
    }

    public T getSize() {
        return this.size;
    }
}

public class ErasureTest {
    public static void main(String[] args) {
        Apple<Integer> a = new Apple<>(6);    // ①
        // a的getSize方法返回Integer对象
        Integer as = a.getSize();
        // 把a对象赋给Apple变量,丢失尖括号里的类型信息
        Apple b = a;      // ②
        // b只知道size的类型是Number
        Number size1 = b.getSize();
        // 下面代码引起编译错误
		Integer size2 = b.getSize();  // ③
    }
}

  上面程序中定义了一个带泛型声明的Apple类,其泛型形参的上限 是Number,这个泛型形参用来定义Apple类的size变量。程序在①处创 建了一个Apple对象,该Apple对象的泛型代表了Integer类型,所以调 用a的getSize()方法时返回Integer类型的值。 当把a赋给一个不带泛 型信息的b变量时,编译器就会丢失a对象的泛型信息,即所有尖括号 里的信息都会丢失—因为Apple的泛型形参的上限是Number类,所以编 译器依然知道b的getSize()方法返回Number类型, 但具体是Number的 哪个子类就不清楚了。
  从逻辑上来看, List是List的子类, 如果直接把一个 List对象赋给一个List对象应该引起编译错误, 但实际上不 会。 对泛型而言, 可以直接把一个List对象赋给一个List对 象,编译器仅仅提示“未经检查的转换”,看下面程序。

public class ErasureTest2 {
    public static void main(String[] args) {
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(9);
        List list = li;
        // 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
        List<String> ls = list;     // ①
        // 但只要访问ls里的元素,如下面代码将引起运行时异常。
        System.out.println(ls.get(0));
    }
}

  上面程序中定义了一个List对象, 这个List 对象保留了集合元素的类型信息。当把这个List对象赋给一 个List类型的list后, 编译器就会丢失前者的泛型信息, 即丢失list 集合里元素的类型信息, 这是典型的擦除。 Java又允许直接把List对 象赋给一个List(Type可以是任何类型)类型的变量, 所以程 序在①处可以编译通过, 只是发出“未经检查的转换”警告。 但对 list变量实际上引用的是List集合,所以当试图把该集合里 的元素当成String类型的对象取出时, 将引发ClassCastException异 常。
  下面代码与上面代码的行为完全相似。

public class ErasureTest2 {
    public static void main(String[] args) {
        List li = new ArrayList<>();
        li.add(6);
        li.add(9);
        System.out.println((String) li.get(0));
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值