读《java编程思想》15-泛型

一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义类型,如果要编写可以应用于多种类型的代码,这种刻板的限制对代码束缚就会很大。
 
看到这句话,第一反应,并不是泛型,而是多态。多态也是一种“泛化”。但是多态受局限于单继承体系或接口。如果我们想编写更加通用的代码,要使代码能够应用于“某个不具体的类型”。于是引入了泛型。
 
泛型即参数化类型:“适用于许多许多类型”
 
既然是泛型也是一种类型参数,就有实参和形参的区分。在定义时为形参,在实际使用时给定实参。
 
 
1、简单泛型类
//一个二元的元组类
public class TwoTuple<A, B> { //A,B就是泛型形参
    public final A first; 
    public final B second;

    public TwoTuple(A a, B b) {
        first = a;
        second = b;
    }

    public String toString() {
        return "(" + first + "," + second + ")";
    }
}
public class Main {
    public static void main(String[] args) {
        TwoTuple<String, Integer> tuple = new TwoTuple<>("hello", 123); //这里省略的<String, Integer>就是泛型实参
        System.out.println(tuple);
    }
}
元组(tuple),是一组对象直接打包存储于其中的单一对象,这个容器对象允许读取其中元素,但不允许修改。(数据传输对象)
 
 
2、泛型接口
//生成器
public interface Generator<T> {
    T next();
}

//实现接口时传入泛型实参
public class NumGen implements Generator<Integer> {
    @Override
    public Integer next() {  //返回值为实参
        return null;
    }
}

//实现接口时不传入泛型实参
public class CoffeeGen<T> implements Generator<T> {
    @Override
    public T next() { //返回值依然为形参
        return null;
    }
}
 
(1)生成器(generator),专门负责创建对象的类,也是工厂设计模式的一种运用。
(2)基本类型不能作为泛型实参,包装类型可以。
 
3、泛型方法
public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gen = new GenericMethods();
        gen.f("hello"); //类型参数推断
//        gen.<String>f("hello"); //显示的类型说明
    }
}
(1)泛型方法和是否为泛型类无关。
(2)要定义泛型方法,只需要将泛型参数列表置于返回值之前。
(3)相比泛型类,优先使用泛型方法。
(4)static方法无法访问泛型类类型参数,只能使用泛型方法。
(5)当使用泛型类时,必须在创建对象的时候指定类型参数的值。而使用泛型方法的时候,通常不必指定参数类型,因为编译器会找出具体类型,这称为类型参数推断。
(6)也可以显示的指明泛型类型,必须在点操作符与方法名之间插入,如上面的<String>。
 
4、擦除的神秘之处
(1)java中的泛型并不是真正的泛型(如和C++对比),为什么这么说?
java中的泛型是应用于编译阶段的类型检查,和运行阶段的类型转化。这个中间的过程被“擦除”了,变成了Object。
如:
public static void main(String[] args) throws Exception {
    List<Integer> list = new ArrayList<>();
    list.add(0);
    //绕过泛型的静态检查插入字母x
    Method add = list.getClass().getMethod("add", Object.class);
    add.invoke(list, "x");
    System.out.println(list); //列表输出正常
    Integer i = list.get(1); //泛型转型失败ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    System.out.println(i);
}
 
(2)在泛型代码内部,无法获得任何有关泛型参数类型的信息。
public class LostInformation {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        List<Double> list = new ArrayList<>();
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
    }
}
看到输出的只是占位符:<K, V>,<E>。无法获得具体的泛型参数类型。
 
(3)可以给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型,这里使用extends关键字,
泛型类型参数将擦除到他的第一个边界(可能会有很多边界)
public class Store <T extends Coffee & Generator> {}
 
注意:
第一个边界必须为类,后面可以跟着接口。虽然T 被擦除为Coffee,但是同时可以使用Coffee,Generator的方法。只是如:void set(T t)的方法, 此时编译器提示参数类型为Coffee,只实现Generator的参数传入,编译器会报错提示参数类型不正确。
 
(4)为什么会有擦除?
擦除并不是一个语言特性,他是java引入泛型实现(新特性)的一种折中,即为了“迁移兼容性”。需要泛型代码和非凡性代码共存(互相调用)。如:
public class Test {
    public void method1(List<String> args) { //泛型参数
        System.out.println(args);
    }

    public void method2(List args) { //原生类型
        System.out.println(args);
    }

    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>(Arrays.asList("a", "b", "c"));
        List list2 = new ArrayList(Arrays.asList("a", 1, "c"));

        Test test = new Test();
        test.method1(list1); 
        test.method1(list2); //泛型参数接收原生类型
        test.method2(list1); //原生类型接收泛型参数
        test.method2(list2);
    }
}
在基于擦除的实现中,泛型类型被当做第二类类型处理,即不能再某项重要的上下文环境中使用,泛型类型只有在静态类型检查期间才出现(编译期),在此之后,程序中所有的泛型类型都将被擦除,替换为它们的非泛型上界。(extends 后第一边界,默认是Object)
 
(5)擦除的问题
不能用于显示地引用运行时类型的操作之中,如:转型,instanceOf,new表达式。因为所有关于参数的类型信息都丢失了。
 
6、擦除的补偿
(1)因为擦除带来的问题,任何运行时需要知道确切类型的操作都将无法工作,如:
public class Erased<T> {
    private final int SIZE = 100;

    public void f(Object arg) {
//        if (arg instanceof T) {} //Error 类型判断
//        T var = new T(); //Error 创建实例对象
//        T[] array = new T[SIZE];  //Error 创建泛型数组
        T array = (T) new Object(); //转型
        System.out.println(array); //无论T是传进来什么类型,输出都是Object,已经被擦除
    }

    public static void main(String[] args) {
        Erased<Date> erased = new Erased<>();
        erased.f(new Date());
    }
}
输出:
 
(2)但是我们可以引入类型标签来对擦除进行补偿,这意味着你需要显示的传递你的类型Class对象,以便可以在类型表达式中使用它。如下:
public class ClassTypeCapture<T> {
    private final int SIZE = 100;
    private Class<T> kind; //类型标签


    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public void f(Object arg) throws Exception {
        System.out.println(kind.isInstance(arg)); //类型判断
        T t = kind.newInstance(); //创建实例对象
        System.out.println(t);
        T[] array = (T[]) Array.newInstance(kind, SIZE); //创建泛型数组
        System.out.println(array);
        T t1 = kind.cast(arg); //转型
        System.out.println(t1);
    }

    public static void main(String[] args) throws Exception {
        ClassTypeCapture<Date> capture = new ClassTypeCapture<>(Date.class);
        capture.f(new Date());
    }
}
输出:
true
Wed Sep 25 15:01:08 CST 2019
[Ljava.util.Date;@1d44bcfa
Wed Sep 25 15:01:08 CST 2019
 
(3)对于kind.newInstance()创建对象实例的方式,如果没有默认的构造函数,运行会报错并且编译时无法检查,如Integer。
因此推荐使用工厂模式:
public interface Factory<T> {
    T create();
}

public class IntegerFactory implements Factory<Integer> {
    @Override
    public Integer create() {
        return new Integer(0);
    }
}

public class StringFactory implements Factory<String> {
    @Override
    public String create() {
        return new String("string");
    }
}

public class FactoryMain {
    static <T, F extends Factory<T>> T newInstance(F factory) {
        return factory.create();
    }

    public static void main(String[] args) {
        Integer i = newInstance(new IntegerFactory());
        System.out.println(i);
        String str = newInstance(new StringFactory());
        System.out.println(str);
    }
}
 
如果说Class.newInstance是一种隐式的工厂,那么FactoryMain.newInstanse就是一种显示的工厂。
 
(4)创建对象实例还可以使用模板方法设计模式
public abstract class Creator<T> {
    private T element;

    public Creator() {
        this.element = create();
    }

    abstract T create(); //模板方法

    public void f() {
        System.out.println(element.getClass().getName());
    }
}

public class IntegerCreator extends Creator<Integer> {
    @Override
    Integer create() {
        return new Integer(0);
    }
}

public class CreatorMain {
    public static void main(String[] args) {
        Creator<Integer> creator = new IntegerCreator();
        creator.f();
    }
}
输出:
java.lang.Integer
 
7、边界
上面已经提到过边界,如:<T extends ArrayList>,它使得你可以在用于泛型参数类型上设置限制条件, 当发生擦除时,会擦除到第一个边界,此时不仅可以使用Object方法,还可以使用边界类ArrayList的方法。
这里重用了extends关键字(super不行),可以有多个边界,如:<T extends C & I1 & I2 & I3>,类必须放第一个,后面为多接口。
 
8、通配符
通配符被限制为单一边界。
 
(1)可以将导出类的数组赋予给基类型的数组引用。
public class Fruit {
}
public class Apple extends Fruit {
}
public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[1] = new Apple();
        fruits[0] = new Fruit(); //可以编译通过,运行报错
    }
}
注意:但是数组的类型实际上是Apple,所以元素可以放入Apple及其子类。放入Fruit编译可以通过,运行会报错ArrayStoreException。泛型正式为了将类型性错误检测移到编译期。于是可以使用泛型容器代替:
 
List<Fruit> fruit = new ArrayList<Apple>(); //Error 编译错误
编译错误的原因是Fruit的list 和 Apple 的list是两种容器的类型,而不是仅仅是不同的持有类型。泛型没有内建的协变类型,他们不能向上转型。
 
备注:
  • 协变:子类能向父类转换
  • 逆变:父类能向子类转换
 
(2)协变通配符
public static void main(String[] args) {
    List<? extends Fruit> fruits = new ArrayList<>();
    fruits.add(new Apple()); //Error 编译错误
    fruits.add(new Fruit()); //Error 编译错误
    fruits.add(null);

    Fruit fruit = fruits.get(0); //安全的
    System.out.println(fruit);
}
这时候如果想要建立向上转型的关系,可以使用导出类通配符<? extends Fruit>,其意义是Fruit及其子类中的任意一种,具体哪一种不清楚,因此Fruit一定可以作为get方法的返回值,但不能set方法的参数(add同理) 。
 
备注:
泛型只是编译器检查,可以有各种方法绕过。比如利用反射向list中添加元素,或者向下面这样:
List<? extends Fruit> fruits = Arrays.asList(new Apple(), new Fruit());
 
(3)逆变通配符
public static void main(String[] args) {
    List<? super Fruit> fruits = Arrays.asList();
    fruits.add(new Apple()); //安全
    fruits.add(new Fruit());  //安全
    fruits.add(null);

    Fruit fruit = fruits.get(0);//Error 编译错误
    System.out.println(fruit);
}
建立关系,还可以使用超类型通配符 <? super Fruit> ,其意义是Fruit及其父类中的任意一种,具体哪一种不清楚,因此Fruit一定可以作为set方法的参数(add同理),但却不能get方法的返回值。
 
注意:
不能对泛型参数给出一个超类型边界,即不能声明<T super MyClass>
 
(4)无界通配符
public static void main(String[] args) {
    List<?> fruits = Arrays.asList();
    fruits.add(new Apple()); //Error 编译错误
    fruits.add(new Fruit()); //Error 编译错误
    fruits.add(null);

    Fruit fruit = fruits.get(0);//Error 编译错误

    fruits.add(new Object()); //Error 编译错误
    Object obj = fruits.get(0); //安全
    System.out.println(obj);
}
List<?> 无界通配符表示“任意事物”中的一种,哪一种还是不知道,因此没有类型能作为set方法的参数(add同理),除了Object,其他类型也都不能作为get方法的返回值。由此可知 List(或List<Object>) 和 List<?> 只是长得很像。
 
 
9、泛型问题
(1)任何基本类型都不能作为类型参数。
 
(2)一个类不能实现同一个泛型接口的两种变体。
如:
interface I1<T> {}
class Base implements I1<String>{}
public class C1 extends Base implements I1<Integer> {} //Error 编译错误
 
(3)转型和警告
使用带有泛型类型的参数的转型或instanceof不会有效果。
 
(4)犹豫擦除原因,泛型不能重载。
如:
public void addAll(List<T> list){}
public void addAll(List<V> list){} //Error 编译错误
 
(5)基类劫持接口
和(2)是一个意思。 Base类确定了接口I1泛型为String,那么C1 继承了Base ,再次实现接口I1,泛型不能是Integer,只能是String(被劫持)
 
10、自限定类型
(1)自限定
public class SelfBounded<T extends SelfBounded<T>> {}
初次看到,可能很难理解,没关系,我们先做下”擦除“,去掉extends 和 T,
public class SelfBounded<SelfBounded> {}
他的意思是SelfBounded的泛型参数是SelfBounded自己,即自己限定自己。
加上<T extends SelfBounded>,只说明泛型参数为SelfBounded及其子类。
 
如:
public class SelfBounded<T extends SelfBounded<T>> {
    T element;

    public SelfBounded<T> set(T element) {
        this.element = element;
        return this;
    }

    public T getElement() {
        return element;
    }
}

class A extends SelfBounded<A> {}
class B extends SelfBounded<A> {} ///也是ok的,因为A 也是SelfBounded
class C {}
class D extends SelfBounded<C> {} //Error 编译错误,因为C并不是SelfBounded 
自限定参数的意义在于:它可以保证类型参数必须与正在被定义的类相同。
 
(2)参数协变
a、自限定类型的价值在于他们可以产生协变参数类型。
b、如果在一对父子类中,某一个方法,方法名称和参数都相同。则为override重写,即子类方法覆盖了父类方法,只有一个方法,多态的体现;方法名称相同,参数不同,则为overload重载,即为两个方法。
c、在方法名称和参数都相同前提下,返回值可以协变,子类方法的返回值 可以是 父类方法 子类(反过来编译报错)。这也算是override重写。
d、正常情况参数是不能协变的,属于overload重载,但是自限定类型的参数却可以做到overload重载。
 
另:
对于”参数协变“这个主题,书上p408的例子SelfBoundingAndCovarianAndCovariantArguments.java,个人觉得更像是一个子类参数自动向上转型为父类,父类参数不能自动向下转型为子类的问题。连同”自限定类型“这一节在内,目前能想到的用途在于做”比较“的业务。有知道其他用途的可以告诉我。
 
11、异常
catch语句不能捕获泛型类型的异常,但可以作为方法签名上throws 异常部分。
如:
interface Test2<T extends Exception> {
    void test() throws T;
}
 
12、混型
顾名思义,就是将多种类型混合起来,使一种类拥有多种能力。
(1)java语法上不支持多继承,那么最简单实现方式,就是单继承多接口 + 代理了(内部类实现多继承也是一种代理)
(2)其次可以使用装饰器模式,类似IO类,缺点是只能有效的工作与装饰中的最后一层。
(3)动态代理可以实现近似的混型。(个人觉得,不如直接使用普通代理)
//说话的能力
interface ISay {
    void say();
}

class SayImpl implements ISay {
    @Override
    public void say() {
        System.out.println("hello");
    }
}

//报时的能力
interface IDate {
    void now();
}

class DateImpl implements IDate {
    @Override
    public void now() {
        System.out.println(new Date());
    }
}

//唱歌的能力
interface ISing {
    void sing();
}

class SingImpl implements ISing {
    @Override
    public void sing() {
        System.out.println("lalalalalala!");
    }
}

class Mix implements InvocationHandler { //动态代理
    private Map<String, Object> delegates = new HashMap<>();

    public Mix(Object ...args) {
        for (Object obj : args) {
            Class<?> clazz = obj.getClass();
            Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                if (!delegates.containsKey(method.getName())) {
                    delegates.put(method.getName(), obj);
                }
            }
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object delegate = delegates.get(method.getName());
        return method.invoke(delegate, args);
    }

    public static Object newInstance(Class[] clazzes, Object[] objects) {
        return Proxy.newProxyInstance(Mix.class.getClassLoader(), clazzes, new Mix(objects));
    }
}


public class Test1 {
    public static void main(String[] args) {
        //混合三种能力
        Object mixObj = Mix.newInstance(
                new Class[]{ISing.class, IDate.class, ISay.class},
                new Object[]{new SingImpl(), new DateImpl(), new SayImpl()});

        ISing singObj = (ISing) mixObj;
        singObj.sing();

        ISay sayObj = (ISay) mixObj;
        sayObj.say();

        IDate dateObj = (IDate) mixObj;
        dateObj.now();
    }
}
 
13、潜在的类型机制及其补偿
潜在类型机制:只要求实现某个方法子集,而不是特定的类或者接口,使得你可以横跨类继承结构,调用不属于公共接口的方法。简单来说,我不关心你是什么类型,只要你可以say() 和 sing() 就可以了。
java语法上显然不支持,但是可以使用反射达到相同的效果。
public class Test3 {
    public static void perform(Object obj){
        Class<?> clazz = obj.getClass();
        try {
            Method say = clazz.getMethod("say");
            say.invoke(obj);
        } catch (Exception e) {
            System.out.println(obj + " can not say");
        }

        try {
            Method say = clazz.getMethod("sing");
            say.invoke(obj);
        } catch (Exception e) {
            System.out.println(obj + " can not sing");
        }
    }

    public static void main(String[] args) {
        perform(new SingImpl());
        System.out.println("----------");
        perform(new SayImpl());
    }
}
输出
chapter15.w1.t3.SingImpl@5e2de80c can not say
lalalalalala!
----------
hello
chapter15.w1.t3.SayImpl@6f94fa3e can not sing
 
14、将函数作为策略
其实就是泛型在策略模式中的使用。
interface Strategy<T, R> {
    R operation(T obj1, T obj2);
}

//整数相加操作
class IntegerAddOperation implements Strategy<Integer, Integer> {
    @Override
    public Integer operation(Integer obj1, Integer obj2) {
        return obj1 + obj2;
    }
}

//字符串比较操作
class StringCompareOperation implements Strategy<String, String> {
    @Override
    public String operation(String obj1, String obj2) {
        Integer result = obj1.compareTo(obj2);
        if ( result == 0) {
            return obj1 + " == " + obj2;
        } else if (result > 0) {
            return obj1 + " > " + obj2;
        } else {
            return obj1 + " < " + obj2;
        }
    }
}

public class Context {
    public static void execute( Object obj1, Object obj2,Strategy strategy) {
        Object result = strategy.operation(obj1, obj2);
        System.out.println(result);
    }

    public static void main(String[] args) {
        execute(1, 2, new IntegerAddOperation());
        execute("today", "tomorrow", new StringCompareOperation());
    }
}
输出
3
today < tomorrow
 
14、题外话
泛型这一章足足有77页,其实真正泛型的知识点并不多,大概可以只看到”8、通配符“就可以结束了,后面都是作者根据在其他语言如C++,Python语言上的特性 对比 联想 出来应用场景及注意事项
,就如上一章中RTTI一样。
 
 
 
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/shineon/p/11599403.html