thinking in java学习记录(十五)泛型

一般的类和方法,只能使用具体的类型;要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。
在面向对象编程语言中,多态算是一种泛化机制。例如你可以将方法 的参数类型设为基类,那么方法就接受从这个基类导出的任何类作为参数
Java SE5开始有泛型的概念,泛型实现了参数化类型

15.1 与C++比较

学习这章的目的是了解泛型的边界(它能做什么不能做什么)

15.2 简单泛型

一个促成泛型生成的原因是容器类。
泛型的主要目的之一就是用来指定容器要持有什么类型的对象,并且由编译器来保证类型的正确性。因此与其用Object,我们更加喜欢暂时不指定类型,而是稍后再决定具体使用什么类型,在java中:List<T> “<>”是泛型标志,T是类型参数,类型参数就是把类型当做参数传入方法。

15.2.1 一个元组类库

仅使用一次返回就能够返回多个对象,而return语句只能返回一个对象,解决方法就是创建一个对象,用它来持有多个想要返回的对象。为了不在每次需要时都重新创建一个新的类,就可以使用泛型。这样的类的概念称为“元组”,它是将一组对象直接打包存储于其中单一对象,这个容器允许读,不允许写。
例如二维元组:

public class TwoTuple<A,B> {
    public final A first;
    public final B second;
    
    public TwoTuple(A first, B second) {
        this.first = first;
        this.second = second;
    }
    
    @Override
    public String toString() {
        return "("+first+","+second+")";
    }
}

虽然我们将first和second设置为了public,但这并不意味着破坏了封装,因为当TwoTuple被创建以后,final保证了其不会再次被赋值。

我们还可以用继承机制创建更长的元组

public class ThreeTuple<A,B,C> extends TwoTuple{
    public final C third;
    
    public ThreeTuple(Object first, Object second, C third) {
        super(first, second);
        this.third = third;
    }
    
    @Override
    public String toString() {
        return "("+first+","+second+","+"third"+")";
    }
}

于是到实际使用时只需要创建一个长度合适的元组,将其作为返回值即可。

15.2.2 一个堆栈类(末尾哨兵)
15.2.3 RandomList

15.3 泛型接口

泛型也可以用于接口。例如生成器,这是一种专门负责创建对象的类。实际上这是工厂设计模式的一种应用。不过当使用生成器创建新的对象时不需要参数,而工厂方法一般需要参数。一般来说一个生成器只定义一个方法,该方法用于产生新的对象,如:

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

要使用时就实现这个接口并传入想要生成的类型参数。

java的泛型有一个局限性:不能使用基本类型作为类型参数,只能使用包装类型

适配器(adapter):通过继承该类并实现想要的功能的接口来将一个不符合要求的类转变为符合要求。

15.4 泛型方法

泛型也可以用于方法,即使这个方法所在的类不是泛型类。而且泛型方法的优先级比泛型类高。

15.4.1 杠杆利用类型推断参数

类型推断值对赋值有效:List<String> list=new ArrayList();后面的ArrayList就不用再次书写泛型了

15.4.2 可变参数与泛型方法

可变参数与泛型方法能够很好的共存

15.4.3 用于Generator的泛型方法
15.4.4 一个通用的Generator

下面的类可以为任意的类创建Generator,只要这个类有默认的构造器。


public class BasicGenerator<T> implements Generator {
    private Class<T> type;
    public BasicGenerator(Class<T> type){
        this.type=type;
    }
    @Override
    public T next() {
        try {
            return type.newInstance();
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
    
    public static <T> Generator<T> create(Class<T> type){
        return new BasicGenerator<T>(type);
    }
}
15.4.5 简化元组的使用
15.4.6 一个Set的实用工具

使用Set的泛型,来做各个集合的(交,并,差等)操作。

15.5 匿名内部类

15.6 构建复杂模型

15.7 擦除的神秘之处

public static void main(String[] args) {
        Class c1=new ArrayList<String>().getClass();
        Class c2=new ArrayList<Integer>().getClass();
        System.out.println(c1==c2);
    }

现实是:在泛型代码内部,无法获得任何有关泛型参数类型的信息。
因此你可以知道诸如类型参数标识符(如<T>)和泛型类型边界(如<? extends T>)这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数.
java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体类型信息都被擦除了,你唯一知道的就是你在使用一个对象,因此List<String>List<Integer>在运行时事实上都是相同的类型,这两种形式都被擦除为他们原生类型。即List

15.7.1 C++的方式

java中由于有了擦除,java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。为了调用f(),我们必须借用泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型,这里重载了extends,如:T extends HasF,这个边界告诉我们这个T必须具有类型HasF或者具有从Hasf导出的子类

我们说泛型类型参数将擦除到它的第一个边界(可能有多个边界);我们还提到了类型参数的擦除,编译器实际上会把类型参数替换为他的擦除,就像上面的T extends HasF一样。T擦除到了HasF,就好像在类的声明中庸HasF替换了T一样。

但这就反应出了一个问题,你完全可以不使用泛型,直接创建接受HasF的类,这就提出了很重要的一点:只有当你希望使用类型参数比某个具体类型(以及它所有的子类更加“泛化”时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助)

但是T extends HasF也不是完全没有用,如:

Class ReturnGenericType<T extends HasF>{
	private T obj;
	public ReturnGenericType(T x){obj=x;}
	public T get(){return obj}
}

这个类中的get()方法将返回T这个类型的精确类型而不是HasF

15.7.2 迁移兼容性

在基于擦除的实现中,泛型类型被当做第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为他们的非泛型上届

擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”,即在JVM中是没有泛型这个概念的,但是通过反编译可以看出泛型信息在JVM中被保存在了注释中。之后的类型信息时运行时动态加上去的

15.7.3 擦除的问题

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为在JVM中所有关于参数的类型信息都丢失了。(泛型代码只是让你看起来好像拥有了有关参数的类型信息而已)

15.7.4 边界处的处理

对于在泛型中创建数组,使用Array.newInstance()是推荐的方式

使用泛型的代码和不使用泛型的代码反编译出来的字节码是一模一样的,尽管我们知道使用泛型后,从get()返回之后的转型消失了,还有传给set()的值在编译期会接受检查。
但是实际上,传给set()的值不需要检查(因为这将由编译期执行,而不会在字节码中体现,因为要传给JVM,JVM没有泛型的概念)。get()也需要转型,但是是编译期帮我们做。所以记住“边界就是发生动作的地方

15.8 擦除的补偿

正如我们看到的,对于泛型,任何运行时需要知道确切类型的操作都无法工作

但通过将类型标签(Class对象)传入方法,那就可以动态的使用isInstance()

15.8.1 创建类型实例

程序对创建一个new T()的尝试将无法实现,部分原因是因为擦除,另一部分是因为不知道T有无默认(无参)构造器

在java中通常是使用工厂对象来代替new方法,在工厂对象中有之前所说的(Class对象),所以它可以动态创建我们需要的泛型对象并返回给我们(即变成了使用传递Class的一种变体)

还有java设计师们希望我们使用显式的factory对象,即为某一类型专门设计一个工厂,而不是所有类使用一个通用工厂(因为有些类没有默认构造器)。但是工厂创建可以通用

15.8.2 泛型数组

不能直接创建泛型数组,一般的解决方法是使用ArrayList。因为数组创建时需要确切的类型,然而类型在运行时已被擦除,想要创建一个泛型数组就要先创建一个Object数组然后将其转型为泛型数组(T[])new Object[10]

15.9 边界

泛型重载了extends关键字,如<T extends Class1 & Interface1>先类后接口
(是不是可以理解为extends后的代码都是修饰T的,T才是我们想要的)

通配符被限制为单一边界,而泛型可以多边界

15.10 通配符

数组的一种特殊行为:可以向导出类数组赋予基类数组的引用

数组在java中的行为被完全定义了(内建了编译器和运行时的检查),但是泛型是系统不知道要做什么。

List<? extends Fruit> flist=new ArrayList<Apple>();

你可以将其读作“具有任何从Fruit继承的类型的列表”。但是,这实际上并不意味着这个List可以持有任何类型的Fruit。通配符"?"引用的是一个具体的类型,因此这句话意味着“某种List,它保存了一种现在没有明确指定,但上限是Fruit的具体类型”。因为在声明时,这种具体的类型还没有明确(即泛型在声明时是不关心你以后是什么具体类型,但?通配符告诉你他是一种具体类型),所以你不能向这个List中插入任何数据,即使你是指向一个合法的List<Apple>

15.10.1 编译器有多聪明

泛型类的设计者来决定哪些调用是安全的,并使用Object类型作为其参数类型

15.10.2 逆变

有上边界extends,那也有下边界super(即?super MyClass)
没有 T super MyClass
通配符有上界,无法修改内容但可以查询,有下界无法查询内容,但可以添加MyClass和MyClass的子类作为数据,但不能添加MyClass的基类,不然会扩宽List

15.10.3 无界通配符

无界通配符<?>看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型,事实上,编译器初看起来是支持这种判断的(编译器很少关心使用的是原生类型还是<?>),但实际上,它是在声明 "我想用Java泛型来编写代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型”

也就是说List<?>的意思是“具有某种特定类型的非原生List,只是我们不知道那种类型是什么”

任何时候使用原生类型,都会放弃编译期检查

15.10.4 捕获转换

向一个<?>方法传递原生类型,那么对编译期来说,可能会推断出其实际的类型参数,于是可以将这个类型参数传给其他需要确切类型参数的方法

15.11 问题

15.11.1任何基本类型都不能作为类型参数

解决方法就是使用自动包装机制。
但Integer[]不能装int对象

15.11.2 实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会变成相同的接口

15.11.3 转型和警告

带有泛型类型参数的转型或instanceof不会有任何效果。应该将各个值存储为Object,并在或去这些值的时,再将他们转型回T

15.11.4 重载

由于擦除的原因,重载方法将会产生相同的类型签名
如下列代码不能编译,想要成功编译就得使用不同的方法名:

public class UserList<W,T>{
	void f(List<T> list){}
	void f(List<W> list){}
}
15.11.5 基类劫持了接口

假设有个Pet类,他可以与其他Pet对象作比较(实现了Comparable接口)

public class ComparablePet implements Comparable<ComparablePet>{
	public int compareTo (ComparablePet arg){return 0;}
}

这看起来ComparablePet的子类也可以作比较,但是Comparable接口一旦确定了ComparablePet参数,那就不能使用这之外的任何对象进行比较

15.12 自限定的类型

在java泛型中会出现一个经常性的惯用法:

class SelfBound <T extends SelfBound<T>>

这句话强调了extends关键字在泛型中的作用和继承中的作用完全不一样

15.12.1 古怪的循环泛型

不能直接继承一个泛型参数,
如:MyClass1 extends T
但是可以继承一个在其自己定义中使用了泛型参数的类,
如:class MyClass2 extends MyClass3<MyClass2>
这段代码的含义其实是:“我在创建一个类,这个类继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数”

然后泛型基类,能做什么呢?Java中的泛型关乎着参数和返回类型,所以在基类中就可以使用导出类型作为基类方法的参数和返回值,还能将导出类作为作用域
即本质是 “基类用导出类代替其参数”这意味着泛型基类作为一种它所有导出类的公共模板,但这些功能的参数和返回值都将使用这个导出类

15.12.2 自限定

强制泛型当做其自己的边界参数来使用

class SelfBounded<T extends SelfBounded<T>>{}
//表示这个T这个类型参数的上限必须是SelfBounded<A>,也就是说传入SelfBounded类
//中的类型参数必须继承自SelfBounded,这就造成了限制
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}
但是不能:
calss F extends SelfBounded{}

自限定保证了类型参数与正在被定义的类相同(强制要求了这一点),所以自限定类型只能强制作用于继承关系。如果使用自限定就表示了:这个类所用的类型参数将与使用这个参数的类局域相同的类型

15.12.3 参数协变

使用自限定还有一个好处就是他们可以产生协变参数类型——方法参数类型会随着子类而变化

15.13 动态类型安全

可以使用静态方法checkedCollection(),checkList(),checkMap(),checkSet,checkSortedMap(),
checkSortedSet()这些方法第一个参数是我们想要确认其中类型的集合,第二个参数是我们希望强制要求的类型。

15.14 异常

由于擦除的原因,将泛型应用于异常是很受限制的,catch语句不能捕获泛型异常,因为在编译期和运行期都要知道明确的异常类型,但是类型参数可以在throws语句中使用

15.15 混型

混型最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类(这往往是我们编程最后的手段,它将使组装多个类变得通俗易懂)

混型的价值之一是,我们可以将特性和行为一致的用于多个类之上,当改变混型类的行为时,这个改变将会应用于所有使用混型的类型之上(有点类似于AOP)

15.15.1 c++中的混型

java泛型不允许直接继承一个泛型参数,因为擦除会忘记其基类型

15.15.2 与接口混合

java使用接口来产生混型的效果:想用哪个功能就继承哪个接口,并为此接口声明一个相应的域,然后在该类中实现所有你想要的功能(但这样会使代码会急速膨胀)

15.15.3 使用装饰器模式

于是就产生了装饰器模式,可以参考《HeadFirst 设计模式》

15.15.4 与动态代理混合

15.16 潜在类型机制

也称为“鸭子类型机制”,即如果它走起来像鸭子,并且叫起来也像鸭子,那么我们就可以把它当做鸭子来对待。(表示,我不关心你是什么类型,只要你有这些方法即可,不要求具体类型,调用时如:anything.speak(),只要有speak()方法就行了),(python和c++支持)

15.17 对缺乏潜在类型机制的补偿

java显然不支持潜在类型机制,但这并不意味着有界泛型代码不能在不同的类型层次结构之间应用。也就是说我们仍旧可以创建真正的泛型代码,如下方法

15.17.1 反射

使用通配符和反射可以达到潜在类型机制如:

class<?> spkr=speakr.getClass();
try{
	Method speak=spkr.getMethod("speak");
	speak.invoke(speaker);//用invoke来调用获取到的这个方法
}catch(){}
15.17.2 将一个方法应用于序列
15.17.3 当你并未碰巧拥有正确的接口时
15.17.4 用适配器仿真潜在类型机制

java泛型并不没有潜在类型机制,而我们需要像现在类型机制这样的东西去编写能够跨类边界应用的代码。潜在类型机制意味着“我不关心我在这里使用的是什么类,他只要有这些方法就行了”。其实就是创建了一个隐式接口(如果我们手动创建了一个接口,这个接口包含了如speak的方法,然后我们在使用一个实现了这个接口的类的时候,就可以用这个接口,面向接口编程,好像就达到了潜在类型机制的样子)

适配器设计模式是从我们拥有的接口中编写代码来产生我们想要的接口

15.18 将函数对象用作策略

策略设计模式,就是定义算法簇,有类似行为的算法实现自一个相同的接口,这个接口只有一个方法,于是就将这个方法隔离到了一个对象中(函数对象)。当然你可以使用普通的对象(含有多个方法的对象),但是使用函数对象的目的是用来区别。比如说Interface Create{} 这个接口就是用来提供创建这个算法簇的

15.19 总结:转型真的如此糟糕吗?

泛型类型机制最吸引人的地方,就是在使用容器的地方,因为泛型就可以让容器保障编译时安全。但事实证明:猫在狗容器里的情况很少很少

发布了28 篇原创文章 · 获赞 1 · 访问量 639

猜你喜欢

转载自blog.csdn.net/c630843901/article/details/103017965
今日推荐