Java学习笔记:泛型

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/sinat_38393872/article/details/93535738

这篇文章是对自己学习的一个总结,学习资料是疯狂Java讲义第三版,李刚编,电子工业出版社出版。

  • 这片文章对泛型的细节进行了进一步的探讨,没写基础。

在Java7以前,定义有泛型的接口时比较繁琐。等号的左右边都需要加上接口可接受的类型,比如下面的例子

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

Java7以后,可以省略等号右边的一部分内容,比如下面的例子

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

以上是最简单的泛型的定义,下面的内容会涉及到定义接口,函数,类的泛型。


定义泛型接口、类

Java5之后定义Map的代码是这样的

public interface Map<K, V>{
    Set<K> keySet();
    V put(K key, V value);
    ...
}

接口的泛型被简写成K和V,当然也可以使用其它的变量名来代替K和V。

自定义接口和类时也可以使用这样的形式,在接口名或类名后加上尖括号,尖括号里就是泛型的名字比如下面的代码

public interface A<aa, bb>{
    aa function1();
    int function2(bb c);
}
public class B<aa, bb>{
    void B(){
        //do something
    }
    aa function1(){...};
    int function2(bb c){...};
}
}

值得注意的是,定义类使用泛型时,定义构造函数时,函数名不要加上尖括号,就正常的写法就行了。

如果泛型参数有多个,并且我们要表现出参数之间有继承关系,比如有两个泛型类型<T, S>,我们要表现出S是T的子类,似乎不能表现出来。看了文章后面我觉得应该这样写

class A<T, S extends T>{
    void test(S a, T b) {
		System.out.println(a);
		System.out.println(b);
	}
}

这种写法好像没问题,也不会报错,但是实例化这个对象后,调用方法test,即使参数a的类型不是b的父类,系统也不会报错,这个代码能运行,输出也是对的。这就意味着,上面那样定义类是有问题的,因为我们想要表现参数的继承关系,结果调用时即使参数没有表现出继承关系系统也不会报错,这就有问题。

A a = new A();
int b = 3;
String c = "dd";
a.test(b, c);

想要表现出参数之间的继承关系似乎只能是有泛型接口的类才可以表现。

但是下面这样的写法对于泛型集合是有效的,比如

protected static <T, S extends T> S test(Collection<T> dest, Collection<S> src){
		return last;
	}
	public static void main(String[] args){
		List<Integer> dest = new ArrayList<>();
		List<Double> src = new ArrayList<>();
		src.add(3.2);
		src.add(2.3);
		Double result = copy(dest, src);    //报错,参数类型不符合定义
		System.out.println(result);
	}

从泛型类派生子类

当一个父类定义了泛型接口,一个子类想要继承这个父类时,要有特定的写法。

比如父类时Apple,子类是A。父类只定义了一个泛型参数,代码如下所示。

public class Apple<T>{...}

子类继承父类时要么不写泛型接口,要么将泛型形参改成具体的形参。下面两种写法都是合法的。

public class A extends Apple{...}
public class A extends Apple<String>{...}

最好是使用第二种方法,明确指出泛型的具体类型。不推荐使用第一种方法,会出现位置的错误。


类型通配符:方法定义中,参数使用泛型

在声明定定义一个方法时,如果参数是一个泛型,会有很多有意思的情况。比如下面代码定义一个方法

public void test(List c) {
	System.out.println("Something");
}

参数c在方法中没有用到,这个不用管。

上面的方法没什么语法问题,但是参数类型List是一个泛型声明的接口,如果我们没有指定类型,那代码可能会有泛型警告。按照之前的习惯,我们加上特定类型的泛型。

public void test(List<Object> c) {
	System.out.println("Something");
}

这样似乎没问题了,符合泛型的规范写法。但是其中的问题很大。比如我们真的调用了这个方法比如下面的代码

List<String> strList = new ArrayList<>();
test<strList>;

这个代码是有编译错误的,test函数中的参数的类型必须是List<Object>或者List<Object>的子类。因为系统认为List<String>和List<Object>不是一个类型,并且List<String>不是List<Object>的子类,所以会出现错误。

在泛型中,即使类Father是类Son的子类,但List<Father>和List<Son>仍然不具有继承关系。所以尽管String是Object的子类,但是List<Object>和List<Object>不具有继承关系。

如果想要表达出List<String>和List<Object>也是有继承关系,就要使用类型通配符,写法就是List<?>。List<?>表达的意义就是任何List类型都是List<?>的子类。上面的test函数就应该写成下面代码。

public void test(List<?> c) {
	System.out.println("Something");
}

这样的话,下面的代码都可以编译成功,因为List<String>和List<Double>都是List<?>的子类。

List<String> strList1 = new ArrayList<>();
List<Double> strList2 = new ArrayList<>();
test<strList1>;
test<strList2>;

但有时候我们不希望List<?>是任何泛型List的父类,我们只希望它是某一具体泛型List的父类,这就需要设定类型通配符的上限。写法很简单,比如我们想要表示参数类型只能是List<Object>或者其子类,代码就写成这个样子。

public void test(List<? extends Object> c) {
	System.out.println("Something");
}
...
List<String> strList1 = new ArrayList<>();
List<B> strList2 = new ArrayList<>();    //B是已定义的类,但不是Object的子类
test<strList1>;        //编译成功
test<strList2>;        //编译失败

这表示如果类A是Object的子类,那么List<A>也是List<Object>的子类;若类A不是Object的子类,那么List<A>不是List<Object>的子类。

上面的这种写法也可以用在类和接口的泛型上,效果是一样的。

public class A<T extends Object>{...}

我们还可以指定泛型是多个接口的子类。

public void test(<? extends Object && Cloneable && Appendable> c){...}

这样的写法必须保证参数类型的第一个继承的是类,后面的是接口。

最后一点,定义一个方法时,若参数a的类型有泛型接口,并且声明时不确定具体的泛型,比如下面的代码。

public void test(List<?> a){...}    //不清楚具体泛型
public void test(List<? extends Object> a){...}    //只知道是继承Object的泛型,但仍然不知道具体的泛型

那么不能在定义方法的代码中含有对a进行添加任何类型的元素的操作,因为系统不能提前知道参数具体的类型,同时Java又规定————像List,Map这些集合,我们应该先确定了这些集合的类型之后才能将对象添加到这些集合。比如下面的代码是会引起编译错误。

public void test(List<?> c) {
    System.out.println("Something");
    c.add(1);
}

这种情况有一个除外,就是没有尖括号的泛型,尽管系统也不知道它的具体泛型,它也可以在方法中添加元素。比如下面代码是可以编译成功的。

public void test(List c) {
    System.out.println("Something");
    c.add(1);            //特殊例子
}
...
public void test(List<String> c) {
    System.out.println("Something");
    c.add("sdfa");        //类型确定
}

定义泛型方法

上面提到方法中参数是泛型,且泛型不确定,我们就不能在该方法中有对泛型参数进行添加元素的操作。如果我们又要使用不确定类型的泛型,又要添加元素,就要用下面介绍的写法。

和在类和接口中定义泛型形参类似,我们在修饰符后面加上一个或若干个尖括号包起来的泛型类型,如下所示

public <T, S> void test(T a, S b){...}

这样的写法输入定义时也是不确定泛型的具体类型,但是这种写法我们其实可以将T和S看成是已知的类型————就把他们看成是已经定义好的具体的类。

这样即使参数a或者参数b是List这样的类,也可以在方法中进行添加元素的操作。

这里注意一点,写代码不要让计算机感到迷惑,比如我们定义这样一个方法。

public <T> void test(T a, T b){...}

定义好这个方法后,我们调用这个方法却这样写

int a = 1;
String b = 2;
test(a, b);

这时就会出错,因为方法的定义里面参数都是T,尽管还不确定具体类型,但是形参a和b应该都是同一种类型。调用的时候填上了两个不同的类型的参数,会导致计算机不知到应该将T视为哪种类型,这会导致错误。


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

上面也说了,泛型方法和类型通配符的区别是泛型方法可以将声明的泛型类型当成是一个具体的普通的类型,就像int,String那样。这里总结一些什么情况下使用泛型方法,什么情况下使用类型通配符。

类型通配符其实是用来灵活地表示泛型的子类化,比如下面的代码。

public void test(List<?> a){...}    //表示参数只要是List,具体什么泛型都无所谓
public void test(List<? extends A> a)    //表示参数必须是List,并且其泛型的具体类型必须是类A或者A的子类

而泛型方法处理可以实现向泛型集合添加元素外,也适用于一个或多个参数的类型依赖关系,或者方法返回值与参数之间的类型依赖关系,比如下面的代码

public <T> void test(List<T> a, List<? extends T> b){...}    //参数b依赖参数a
public <T> T test(List<T> a){    //返回值依赖参数a
    ···
    return c;    //c的类型是T
}    

ArrayList<String>和ArrayList的关系

前面说ArrayList<String>可以看成是ArrayList的子类,但ArrayList和ArrayList<String>是同一种类,他们拥有同样的Class来描述他们。下面的代码输出是true就证明了这一点。

List<String> l1 = new ArrayList<>();
List<> l2 = new ArrayList();
System.out.println(l1.getClass() == l2.getClass);

在静态变量,静态方法,静态初始化块的声明和使用中不能使用泛型。别问为什么,书上解释我没理解。


泛型构造器

泛型构造器和一般的方法使用泛型差不多的,值得注意的点就是调用泛型构造器的情况和一般的调用构造器时有所不同。现在给类A定义一个泛型构造器如下

class A <E> {
    <T> A(T a){...}
}

E是类A中的泛型,T是构造器中的泛型。我们开始使用构造器初始化类。这种类有泛型,其构造器也有泛型,初始化类的写法可以有一下几种,和一般的构造器语法有所不同。

使用泛型构造器有省略构造器泛型的写法

new A(a);    //a是已经定义好的变量

或者不省略构造器泛型

new <String> A(a);

这里注意一下,只有构造方法才能这样写,其它的泛型方法是不能这样显示指定泛型具体类型的。

如果类也是有泛型接口,那么初始化这个类的语法就是下面的两种情况,和一般的情况也不太一样,需要注意。

第一种:省略构造器泛型。这种写法就是一般的菱形语法,虽然没有指明构造器的泛型类型,但是实参告诉我们其类型是Integer。

A<String> a = new A<>(3);    //这个显示地指定构造器的泛型构造器的泛型类型,但参数指明了其类型是Integer

第二种:显示写出构造器泛型的具体类型。这里要注意,显示写出构造器泛型的话,就不能有菱形语法,下面的这种写法是会报错的。

A<Strig> a = new <Integer> A<>(a);

必须用老版的泛型类初始化方法,不能省略地写

A<String> a = new <Integer> A<String>(3);    

设定通配符的下限

设定通配符的上限就是<? extends A>,就是设定子类。那么设定下限就是设定父类,写法就是<? super A>。我这里举一个例子来展示一个通配符下限比通配符上限好用的应用场景。

我们写一个方法,用通配符的上限来写,将src集合元素复制到dest集合中,并且返回最后一个复制的元素。代码如下。

public static <T> T copy(Collection<T> src, Collection<? extends T> dest) {
    T last = null;
    for(T ele : src) {
        last = ele;
        dest.add(ele);
    }
    return last;
}

只能是这样写,src里的元素必须是dest里的元素的父类或者两者是同类,不然不能复制。看起来好像没问题,但是这有个缺点,返回值是T。举个例子字,调用方法的代码如下

List<Number> dest = new ArrayList<>();
List<Double> src = new ArrayList<>();
Double result = (Double) copy(dest, src);

上面copy()的返回值是Number而不是Double,复制过程中我们丢失了复制元素的类型。我们复制当然是想元素的所有信息都复制过去,上面那种写法还需要我们强制转换一下类型,不然会发生编译错误。有这一点不好。

我们可以用通配符的下限来解决这个问题(其它的也可以,比如<T, S extends T>也可以解决上述问题。)

public static <T> T copy(Collection<T> src, Collection< ? super T> dest){
    T last = null; 
    for(T ele : src){
        last = ele;
        dest.add(ele);
    }
    return last;
}

这样问题就解决了。Java文档中很多地方就是使用通配符下限的手法来写的,比如TreeSet,TreeMap。

这里提一点,关于通配符的上限和通配符的下限的混合使用的问题。记住不要让计算机感到疑惑。比如一个类中有下面的两个方法。

public <T> void copy(Collection<T>, Collection<? extends T>){...}
public <T> void copy(Collection<? super T>, Collection <T>){...}

我们调用copy(Collection a, List b),两个方法都可以调用,这会让计算机感到迷惑,会引起编译错误。

猜你喜欢

转载自blog.csdn.net/sinat_38393872/article/details/93535738