Java泛型:泛型类、泛型接口、泛型方法

399 篇文章 12 订阅
143 篇文章 1 订阅

文章目录

泛型类
    泛型类的继承
    泛型与内部类
泛型接口
    泛型接口的实现
泛型方法
    形参的类型参数通过实参确定
    泛型方法被多个形参推断
    返回值的类型参数通过方法返回值赋值的对象确定
    形参和返回值用了同一个类型参数
    泛型方法定义中的类型推断
    静态方法不可以使用泛型类定义的类型参数
    泛型方法返回值赋值给形参
    可变参数和泛型方法
泛型类和泛型方法
    泛型类中的成员泛型方法和静态泛型方法
匿名内部类和泛型
个人理解总结

泛型按照使用方法分为三种情况:

  1. 泛型类
  2. 泛型接口
  3. 泛型方法

一、泛型类

  1. 泛型允许我们使用类型参数Type parameter,它用一个自定义的标识符(T、V、E)来代表一个类型,用< >括住,放在类名后面。然后在类定义中所有使用该类型的地方都用这个标识符来代替,因为在定义这个类的时候还不知道这个具体类型到底是哪个类型。
  2. 可使用类型参数Type parameter的地方:声明变量、形参、返回值、强制类型转换。注意不可以用于构造器。
  3. 使用泛型类时(区别于定义泛型类时),< >里面的类型必须被具体类型或定义泛型类时的类型参数所确定。如果是被定义泛型类时的类型参数确定的这种情况,则是把确定类型的任务交给了定义泛型类,这种情况常出现在泛型类的继承、或内部类。
  4. 泛型与多态不冲突。当使用泛型类class store<phone>时,你既可以使用phone,也可以使用vivoPhone

泛型类的继承

class two<A,B> {//定义泛型类
}
class three<C> extends two<A, B> {//报错can't resolve symbol 'A',can't resolve symbol 'B'
}

class two<A,B> {
}
class three<A,B,C> extends two<A, B> {
}

在第一段代码中,类three的声明会报错,而第二段不会。这是因为,class three<A,B,C>属于声明泛型类,而extends two<A, B>属于使用泛型类。使用泛型类的时候类型参数必须被确定,这里extends two<A, B>中的A,B被泛型类class three<A,B,C>的声明里的A,B所确定。

虽然extends two<A, B>中的A,B不是具体类型却还是类型参数,但有了声明泛型类class three<A,B,C>的保证,A,B最终能被具体类型所确定。

泛型与内部类

此例实现了一个简单的栈,内部原理是链式存储。

//A stack implemented with an internal linked structure.
public class LinkedStack<T> {//声明泛型类
    private static class Node<U> {//声明泛型类
        U item;
        Node<U> next;
        
        Node() {
            item = null;
            next = null;
        }
        
        Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }
        
        boolean end() {
            return item == null && next == null;
        }
    }

    //使用泛型类,但这里被定义泛型类的类型参数确定
    private Node<T> top = new Node<T>();
    public void push(T item) {
        top = new Node<T>(item, top);
    }
    
    public T pop() {
        T result = top.item;
        if(!top.end()) {
            top = top.next;
        }
        return result;
    }
    
    public static void main(String[] args) {
        LinkedStack<String> lss = new LinkedStack<String>();
        for(String s : "Phasers on stun!".split(" ")) {
            lss.push(s);
        }
        String s;
        while((s = lss.pop()) != null) {
            System.out.println(s);
        }
    }
}
/* Output:
stun!
on
Phasers
*/

类LinkedStack中有个内部类Node,它们都是泛型类。在定义泛型类Node时,类型参数没有被确定,当然也不需要被确定。在使用泛型类private Node<T> top = new Node<T>()时,Node的类型参数被定义泛型类LinkedStack的T所确定,因为这句private Node<T> top = new Node<T>()处于LinkedStack的类定义中,所以这样用也合理。

二、泛型接口

  1. 各方面和泛型类都是类似的。自定义的标识符(T、V、E)来代表一个类型,用< >括住,放在接口名后面。
  2. 使用泛型接口时,< >里面的类型必须被具体类型、定义泛型类或泛型接口时的类型参数所确定。不过一般泛型接口多被具体类型确定。

泛型接口的实现

interface two<A>{
}

class three<A,B> implements two<A>{
}

interface four<A,B> extends two<A>{
}

上例说明了使用泛型接口时,可以被定义泛型类或泛型接口时的类型参数所确定。

interface two<A>{
    A f(A a);
}
class impl implements two<Object>{
    public Object f(Object a){
        return new Object();
    }
}

当使用泛型接口时,确定好的具体类型要覆盖掉原来的所有使用类型参数的地方。比如类impl中的f方法中,所有使用A的地方都要用Object替换掉。

三、泛型方法

  1. 自定义的标识符(T、V、E)来代表一个类型,用< >括住,放在方法返回值前面。可以被用到形参声明、方法返回值、方法定义中的变量声明和类型转换。
  2. 泛型方法使得该泛型方法的类型参数独立于类而产生变化。泛型方法和泛型类没有关系。
  3. 泛型方法的类型参数,一般情况下都是被推断inference出来。更具体地讲,只能被形参或返回值推断出来,当形参和返回值用了同一个类型参数时,二者推断出来的类型必须一样、或者符合多态。
  4. 形参的类型参数通过实参确定;返回值的类型参数通过方法返回值赋值的对象确定。这也就是类型参数推断
  5. 当形参的类型参数和返回值的类型参数是同一个时,优先使用形参的推断。因为返回值的类型参数的推断是一种拖延行为。
  6. 类的成员方法可以使用定义泛型类的类型参数(注意,这种方法不是泛型方法,只不过使用了类型参数而已);而类的静态方法不可以使用泛型类的类型参数,这是因为只有当创建泛型类对象时类型参数才会被具体类型确定,也就是说,泛型类的类型参数是与对象相关的。那么,很自然地,作为一个static方法肯定不可以使用泛型类的类型参数。static方法想用到泛型只能将其定义为泛型方法。

形参的类型参数通过实参确定

class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }
    public void testInt(){
        f(1);
    }
}

public class testP {
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
        gm.testInt();
    }
}

f方法是一个泛型方法,返回值是void,形参是< >里的T。调用泛型方法,既可以通过对象来调用,例如gm.f("");,此时T被推断为String类型;也可以在除该泛型方法外,且类定义中调用,例如testInt()方法中,此时T被推断为int类型。
泛型方法和自动打包让f方法看起来像是被无限重载了一样。

泛型方法被多个形参推断

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

        /**依靠类型参数推断*/
        //这两个参数都是Integer,所以T为Integer类型
        int i = Test.add(1, 2);
        //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
        Number f = Test.add(1, 1.2);
        //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
        Object o = Test.add(1, "asd");

        /**显式的类型说明*/
        int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类
        int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float
        Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
    }

    //这是一个简单的泛型方法
    public static <T> T add(T x, T y) {
        return y;
    }
}

add方法的两个形参都使用到了类型参数T,即使用了同一个类型参数。那么在推断的时候会依靠两个实参的共同最小父类来推断(就好像数学里的最小公倍数)。

返回值的类型参数通过方法返回值赋值的对象确定

class GenericMethods {
    public <T> T f2(Object x) {
        System.out.println(x.getClass().getName());
        return (T)x;
    }
}

public class testP {
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        int o1 = gm.f2(2);
        String o2 = gm.f2(2);//能通过编译,但运行时报错
        gm.f2(1.2);//
    }
}

f2方法是一个泛型方法,返回值是< >里的T,形参是Object x。

  1. 执行int o1 = gm.f2(2)后,由于f2方法返回值赋值给了一个int变量,那么这里T就被推断为了int,并且在f2方法的运行过程中,所有使用T的地方都会替换为int,所以return (T)x;实际是执行的return (int)x;。所以,整个运行过程是,2被向上转型为Object,然后强行cast为了int,最后被返回出来。
  2. 执行String o2 = gm.f2(2)后,由于这里T就被推断为了String,一个实际为2的Object对象在向上转型为String类型时,会受到RTTI的检查,被发现无法转型后便报错。
  3. 执行gm.f2(1.2)后,由于返回值没有赋值给对象,所以T没有被推断出来,也不需要被推断出来。因为没有进行赋值,返回值并不关心。

形参和返回值用了同一个类型参数

class A{}
class B extends A{}

class GenericMethods {
    public <T> T f1(T x) {
        System.out.println(x.getClass().getName());
        return x;
    }
}

public class testP {
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        int i = gm.f1(1);
        //String s = gm.f1(1);
        A a = gm.f1(new B());
        //B b = gm.f1(new A());
    }
}

f1方法是一个泛型方法,返回值是< >里的T,形参也是< >里的T。这种情况只使用形参的类型推断,因为靠返回值推断是一种推延行为。

  1. 执行int i = gm.f1(1)时,形参处的T被推断为int,所以没有问题。
  2. 执行String s = gm.f1(1)时,形参处的T被推断为int,然后返回值int赋值给一个String类型,所以这里编译报错。
  3. 执行A a = gm.f1(new B())时,形参处的T被推断为B。然后返回值B对象赋值给一个A类型,这里向上转型。
  4. 执行B b = gm.f1(new A())时,形参处的T被推断为A。然后返回值A对象赋值给一个B类型,这里向下转型,不可以,编译报错。
  5. 这里如果是B b = (B) gm.f1(new A())则能够编译通过。这里把工作交给了强制类型转换,编译器便不会报错了,但运行报错。

泛型方法定义中的类型推断

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class New {
    public static <K,V> Map map() {
        return new HashMap<K,V>();//泛型方法定义中使用类型参数
    }
    public static <T> List list() {
        return new ArrayList<T>();
    }
    public static void main(String[] args) {
        Map<String,List<String>> sls = New.map();
        List<String> ls = New.list();
    }
}

注意这种情况一般都是发生在return语句里。通过泛型方法的赋值,推断出了泛型方法定义中的类型参数的具体类型。

静态方法不可以使用泛型类定义的类型参数

class two<A> {
    public void f(A a) {//只是一个使用了类型参数的成员方法
        System.out.println(a);
    }

    //public static void f1(A a){//静态方法不可以使用泛型类定义的类型参数
    //	System.out.println(a);
    //}

    public static <A> void staticF(A a) {//泛型方法,静态方法
        System.out.println(a);
    }

    public <A> void memberF(A a) {//泛型方法,成员方法
        System.out.println(a);
    }
}

注意staticF方法里的A和two<A>里的A没有任何关系。同样,memberF方法里的A和two<A>里的A没有任何关系。因为这二者都是泛型方法。

two<A>里的A是与对象相关的,只有创建对象时才能确定。f1是静态方法,自然不可能与对象相关的东西有关系。

泛型方法返回值赋值给形参

这种情况区别于泛型方法返回值赋值给变量。

import java.util.HashMap;
import java.util.Map;

class LimitsOfInference {
    static void f(Map<String,Integer> petPeople) {

    }
    public static void main(String[] args) {
         f(New.map()); //报一个警告
    }
}

public class New {
    public static <K,V> Map map() {
        return new HashMap<K,V>();
    }
}

警告内容为: 

 在这里插入图片描述

说明泛型方法中的类型参数并没有被推断出来,所以这里泛型方法返回值只知道是个Map

而为了这里不报警告,则需要使用显示的类型说明,在调用泛型方法时,在点操作符和方法名之间插入尖括号,然后把具体类型放在里面。这种语法很少见,在这样的非赋值语句中,才需要用到,一般用类型参数推断就可以解决。

import java.util.HashMap;
import java.util.Map;

class LimitsOfInference {
    static void f(Map<String,Integer> petPeople) {

    }
    public static void main(String[] args) {
         f(New.<String,Integer>map()); //不报任何警告
    }
}
public class New {
    public static <K,V> Map map() {
        return new HashMap<K,V>();
    }
}

可变参数和泛型方法

import java.util.*;

public class GenericVarargs {
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        for(T item : args)
            result.add(item);
        return result;
    }
    public static void main(String[] args) {
        List<String> ls = makeList("A");
        System.out.println(ls);
        ls = makeList("A", "B", "C");
        System.out.println(ls);
        ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
        System.out.println(ls);
    }
} 
/* Output:
[A]
[A, B, C]
[, A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
*/

makeList方法的功能类似于java.util.Arrays.asList()的功能。

四、泛型类和泛型方法

此例中泛型类和泛型方法同时存在。有一个泛型接口Generator用来产生某种对象。CountedObject类必须有默认构造器因为newInstance。

interface Generator<T> {
    T next();
}

class CountedObject {
    private static long counter = 0;
    private final long id = counter++;// 用于记录创建对象的次数
    public long getId() {
        return id;
    }
    public String toString() {
        return "CountedObject" + id;
    }
}

public class BasicGenerator<T> implements Generator<T> {
    private Class<T> type;
    public BasicGenerator(Class<T> type) {
        this.type = type;
    }
    
    @Override
    public T next() {
        try {
            return type.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
    
    public static <T> Generator<T> create(Class<T> type) {//静态泛型方法
        return new BasicGenerator<T>(type);
    }
  
    public static void main(String[] args) {
        Generator<CountedObject> gen = BasicGenerator.create(CountedObject.class);
        for (int i = 0; i < 5; i++) {
            System.out.println(gen.next());
        }
    }
}
  1. create方法是静态的泛型方法,而泛型类 BasicGenerator 的类型参数是与对象相关的,所以 create方法里的 T 不是泛型类的 T。或者说,在create方法里,create方法的<T>暂时隐藏了BasicGenerator<T><T>,这使得泛型方法的类型参数独立于对象。
  2. 此例体现了泛型方法的好处,由于泛型方法的类型参数推断,所以这里允许执行BasicGenerator.create(CountedObject.class),而不是去执行麻烦的new BasicGenerator<CountedObject>(CountedObject.class)。简单的说,让你少写了尖括号。
  3. 注意一下create方法的return语句和返回值,可以看出Generator<CountedObject>BasicGenerator<CountedObject>的父类。注意这种与泛型有关的多态。

泛型类中的成员泛型方法和静态泛型方法

对上例进行修改,使其同时拥有成员的泛型方法和静态的泛型方法,且这两种泛型方法使用的类型参数和泛型类的类型参数是同一个(都用的T)。staticCreatememberCreate的返回值都进行了修改,使其返回子类BasicGenerator而不是父接口Generator,这是为了对象方便调用新增加的成员方法。

//前面省略
public class BasicGenerator<T> implements Generator<T> {
    private Class<T> type;
    public BasicGenerator(Class<T> type) {
        this.type = type;
    }

    @Override
    public T next() {
        try {
            return type.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> BasicGenerator<T> staticCreate(Class<T> type) {//静态的泛型方法
        return new BasicGenerator<T>(type);
    }

    public <T> BasicGenerator<T> memberCreate(Class<T> type) {//成员的泛型方法
        return new BasicGenerator<T>(type);
    }

    public <T> void test (T t) {//成员的泛型方法
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {
        BasicGenerator<CountedObject> gen = BasicGenerator.staticCreate(CountedObject.class);
        for (int i = 0; i < 5; i++) {
            System.out.println(gen.next());
        }

        //这里独立于类,泛型方法test推断出了自己的类型参数T为String,而不是gen对象确定的CountedObject
        gen.test("");
        gen.test(1);
        BasicGenerator<String> gen1 = gen.memberCreate(String.class);
        System.out.println(gen1.type.getName());
    }
}
/*output:
CountedObject0
CountedObject1
CountedObject2
CountedObject3
CountedObject4
java.lang.String
java.lang.Integer
java.lang.String
*/

从运行结果可以看出:泛型方法不管是静态的还是成员的,就算和泛型类使用了同一个类型参数T,在泛型方法中也会暂时隐藏泛型类的类型参数,而使用自己的类型参数,并且把确定具体类型的任务交给了类型参数推断。这样,就做到了泛型方法能够独立于泛型类的类型参数而产生变化

五、匿名内部类和泛型

本例使用了匿名内部类来继承并实现泛型接口。

interface Generator<T> {//定义泛型接口
    T next();
}

class Customer {
    private static long counter = 1;
    private final long id = counter++;
    private Customer() {}
    public String toString() { return "Customer " + id; }

    // A method to produce Generator objects:
    public static Generator<Customer> generator() {
        return new Generator<Customer>() {//使用泛型接口
            public Customer next() { return new Customer(); }
        };
    }
} 

return new Generator<Customer>()这里属于使用泛型接口,既然是使用,那么必须把类型参数确定下来,并且把使用了类型参数的地方进行替换,替换为Customer。继承并实现泛型类的匿名内部类同理。

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

泛型方法VS类型通配符(两者可以混用):

1、你会发现所有能用类型通配符(?)解决的问题都能用泛型方法解决,并且泛型方法可以解决的更好:

最典型的一个例子就是:

  • 类型通配符:void func(List<? extends A> list);
  • 完全可以用泛型方法完美解决:<T extends A> void func(List<T> list);

上面两种方法可以达到相同的效果(?可以代表范围内任意类型,而T也可以传入范围内的任意类型实参),并且泛型方法更进一步,?泛型对象是只读的,而泛型方法里的泛型对象是可修改的,即List<T> list中的list是可修改的!!

2、要说两者最明显的区别就是:

  • ?泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;
  • 而泛型方法中的泛型参数对象是可修改的,因为类型参数T是确定的(在调用方法时确定),因为T可以用范围内任意类型指定;

注意,前者是代表,后者是指定,指定就是确定的意思,而代表却不知道代表谁,可以代表范围内所有类型;

3、这样好像说的通配符?一无是处,但是并不是这样,Java设计类型通配符?是有道理的,首先一个最明显的优点就是?的书写要比泛型方法简洁,无需先声明类型参数,其次它们有各自的应用场景:

  • 一般只读就用?,要修改就用泛型方法,例如一个进行修改的典型的泛型方法的例子:
public <T> void func(List<T> list, T t) {
    list.add(t);
}
  • 在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该是通配符?:

具体讲就是,如果一个方法的返回值、某些参数的类型依赖另一个参数的类型就应该使用泛型方法,因为被依赖的类型如果是不确定的?,那么其他元素就无法依赖它),例如:<T> void func(List<? extends T> list, T t);  即第一个参数依赖第二个参数的类型(第一个参数list的类型参数必须是第二个参数的类型或者其子类);

可以看到,Java支持泛型方法和?混用;

这个方法也可以写成:<T, E extends T> void func(List<E> list, T t);  // 明显意义是一样的,只不过这个list可以修改,而上一个list无法修改

总之就是一旦返回值、形参之间存在类型依赖关系就只能使用泛型方法;否则就应该使用? ;

4、对泛型方法的类型参数进行规约:即有时候可能不必使用泛型方法的地方你不小心麻烦地写成了泛型方法,而此时你可以将其规约成使用?的最简形式

  • 总结地来讲就是一句话:只出现一次 & 对它没有任何依赖
  • 例如:<T, E extends T> void func(List<T> l1, List<E> l2);  // 这里E只在形参中出现了一次(类型参数声明不算),并且没有任何其他东西(方法形参、返回值)依赖它,那么就可以把E规约成?

最终规约的结果就是:<T> void func(List<T> l1, List<? extends T> l2);

5、一个最典型的应用就是容器赋值方法(Java的API):public static <T> void Collections.copy(List<T> dest, List<? extends T> src) { ... }

从src拷贝到dest,那么dest最好是src的类型或者其父类,因为这样才能类型兼容,并且src只是读取,没必要做修改,因此使用?还可以强制避免你对src做不必要的修改,增加的安全性。

七、个人理解总结

  1. 泛型的类型参数看起来就像是让代码变成了一套模板,当给定具体类型时,模板里的类型参数就会被替换以生效。
  2. 泛型这种类似“模板”的灵活性,在使用泛型时(生成泛型类对象)得以体现,但在使用泛型后(在泛型类对象上调用方法)失去。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值