当前位置:网站首页>第15章 泛型
第15章 泛型
2022-08-02 09:15:00 【@来杯咖啡】
前言
一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大°。
1、我们如何让程序变得更加通用?
1、通过多态的方式放松束缚
在面向对象编程话言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数。这样的方法更加通用一些,可应用的地方也多一些。在类的内部也是如此,凡是需要说明类型的地方,如果都使用基类,确实能够具备更好的灵活性。但是,考虑到除了final类不能扩展,其他任何类都可以被扩展,所以这种灵活性大多数时候也会有一些性能损耗。
有时候,拘泥于单继承体系,也会使程序受限太多。如果方法的参数是一个接口,而不是一个类,这种限制就放松了许多。因为任何实现了该接口的类都能够满足该方法,这也包括暂时还不存在的类。
可是有的时候,即便使用了接口,对程序的约束也还是太强了。因为一但指明了接口,它就要求你的代码必须使用特定的接口。而我们希望达到的目的是编写更通用的代码,要使代码能够应用于“某种不具体的类型”,而不是一个具体的接口或类。
这就是Java SES的重大变化之一:泛型的概念。
2、引出泛型
泛型实现了参数化类型的概念,使代码可以应用于多种类型。
本章主题
介绍和讨论Java泛型的优点和缺点。
15.2 简单泛型
1.1 泛型出现的主要原因
有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类。
(关于容器类,你可以参考第11章和第17章这两章。)容器,就是存放要使用的对象的地方。数组也是如此,不过与简单的数组相比,容器类更加灵活,具备更多不同的功能。事实上,所有的程序,在运行时都要求你持有一大堆对象,所以,容器类算得上最具重用性的类库之一。
1.2 单对象定义示例(演变史)
从定义’具体类型‘-->’Object类型‘-->泛型。
1.2.1 单对象-具体类型
我们先来看看一个只能持有单个对象的类。当然了,这个类可以明确指定其持有的对象的类型:
//: generics/Holder1.java
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
} ///:~
不过,这个类的可重用性就不怎么样了,它无法持有其他类型的任何对象。我们可不希望为碰到的每个类型都编写一个新的类。
1.2.2 单对象-object类型
在Java SES之前,我们可以让这个类直接持有Objeet类型的对象:
//: generics/Holder2.java
public class Holder2 {
private Object a;
public Holder2(Object a) { this.a = a; }
public void set(Object a) { this.a = a; }
public Object get() { return a; }
public static void main(String[] args) {
Holder2 h2 = new Holder2(new Automobile());
Automobile a = (Automobile)h2.get();
h2.set("Not an Automobile");
String s = (String)h2.get();
h2.set(1); // Autoboxes to Integer
Integer x = (Integer)h2.get();
}
} ///:~
现在,Holder2可以存储任何类型的对象,在这个例子中,只用了一个Holder2对象,却先后三次存储了三种不同类型的对象。
有些情况下,我们确实希望容器能够同时持有多种类型的对象。但是,通常而言,我们只会使用容器来存储一种类型的对象。
1.2.3 单对象-类型参数 && 泛型
泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么米型。
要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型替换此类型参数。在下面的例子中,T就是类型参数:
//: generics/Holder3.java
public class Holder3<T> {
private T a;
public Holder3(T a) { this.a = a; }
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
Holder3<Automobile> h3 =
new Holder3<Automobile>(new Automobile());
Automobile a = h3.get(); // No cast needed
// h3.set("Not an Automobile"); // Error
// h3.set(1); // Error
}
} ///:~
现在,当你创建Holder3对象时,必须指明想持有什么类型的对象,将其置于尖括号内。就像main(中那样。然后,你就只能在Holder3中存入该类型(或其子类,因为多态与泛型不冲突)的对象了。并且,在你从Holder3中取出它持有的对象时,自动地就是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
一般而言,你可以认为泛型与其他的类型差不多,只不过它们碰巧有类型参数罢了。稍后我们会看到,在使用泛型时,我们只需指定它们的名称以及类型参数列表即可。
1.3 一个元组类库
仅一次方法调用就能返回多个对象,你应该经常需要这样的功能吧。可是return语句只允许返回单个对象,因此,解决办法就是创建一个对象,用它来持有想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。可是有了泛型,我们就能够一次性地解决该问题,以后再也不用在这个问题上浪费时间了。同时,我们在编译期就能确保类型安全。
这个概念称为元组(uple),它是將一组对象直接打包存储于其中的一个单一对象。这个容器对象允许读取其中元素,但是不允许向其中存放新的对象。(这个概念也称为数据传送对象,或信使。)
通常,元组可以具有任意长度,同时,元组中的对象可以是任意不同的类型。不过,我们希望能够为每一个对象指明其类型,并且从容器中谈取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面的程序是一个2维元组,它能够持有两个对象:
//: net/mindview/util/TwoTuple.java
package net.mindview.util;
public class TwoTuple<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 + ")";
}
} ///:~
我们可以利用继承机制实现长度更长的元组。从下面的例子中可以看到,增加类型参数是件很简单的事情:
//: net/mindview/util/ThreeTuple.java
package net.mindview.util;
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A a, B b, C c) {
super(a, b);
third = c;
}
public String toString() {
return "(" + first + ", " + second + ", " + third +")";
}
} ///:~
//: net/mindview/util/FourTuple.java
package net.mindview.util;
public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C> {
public final D fourth;
public FourTuple(A a, B b, C c, D d) {
super(a, b, c);
fourth = d;
}
public String toString() {
return "(" + first + ", " + second + ", " +
third + ", " + fourth + ")";
}
} ///:~
//: net/mindview/util/FiveTuple.java
package net.mindview.util;
public class FiveTuple<A,B,C,D,E>
extends FourTuple<A,B,C,D> {
public final E fifth;
public FiveTuple(A a, B b, C c, D d, E e) {
super(a, b, c, d);
fifth = e;
}
public String toString() {
return "(" + first + ", " + second + ", " +
third + ", " + fourth + ", " + fifth + ")";
}
} ///:~使用元组:
//: generics/TupleTest.java
import net.mindview.util.*;
class Amphibian {}
class Vehicle {}
public class TupleTest {
static TwoTuple<String,Integer> f() {
// Autoboxing converts the int to Integer:
return new TwoTuple<String,Integer>("hi", 47);
}
static ThreeTuple<Amphibian,String,Integer> g() {
return new ThreeTuple<Amphibian, String, Integer>(
new Amphibian(), "hi", 47);
}
static
FourTuple<Vehicle,Amphibian,String,Integer> h() {
return
new FourTuple<Vehicle,Amphibian,String,Integer>(
new Vehicle(), new Amphibian(), "hi", 47);
}
static
FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
return new
FiveTuple<Vehicle,Amphibian,String,Integer,Double>(
new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
public static void main(String[] args) {
TwoTuple<String,Integer> ttsi = f();
System.out.println(ttsi);
// ttsi.first = "there"; // Compile error: final
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
} /* Output: (80% match)
(hi, 47)
([email protected], hi, 47)
([email protected], [email protected], hi, 47)
([email protected], [email protected], hi, 47, 11.1)
*///:~通过ttsi.first ="there"语句的错误,我们可以看出,final声明确实能够保护public元素,在对象被构造出来之后,声明为final的元素便不能被再赋予其他值了。
在上面的程序中,new表达式确实有点罗嗉。本章稍后会介绍,如何利用泛型方法简化这样的表达式。
15.3 泛型接口
泛型也可以应用于接口。例如生成器(generator),这是一种专门负责创建对象的类。实际上,这是工厂方法设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。也就是说,生成器无需额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,该方法用以产生新的对象。在这里,就是next0方法。我将它收录在我的标准工具类库中:
//: net/mindview/util/Generator.java
// A generic interface.
package net.mindview.util;
public interface Generator<T> { T next(); } ///:~具体案例略(实现类、使用类)。
15.4 泛型方法
到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量选择使用泛型方法,最后选择使用泛型类。另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。要定义泛型方法,只需将泛型参数列表置于返回值之前,就像下面这样:
//: generics/GenericMethods.java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
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);
}
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型。这称为类型参数推断(typeargument inference)。因此,我们可以像调用普通方法一样调用f(),而且就好像是f()被无限次地重载过。它甚至可以接受GenericMethods作为其类型参数。
如果调用f()时传入基本类型,自动打包机制就会介入其中,将基本类型的值包装为对应的对象。事实上,泛型方法与自动打包避免了许多以前我们不得不自己编写出来的代码。
1.1 可变参数与泛型方法
//: generics/GenericVarargs.java
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()方法相同的功能。
15.7 擦除的神秘之处
1.1 Java泛型是通过类型擦除来实现的
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此ListeString>和ListeInteger>在运行时事实上是相同的类型。这两种形式都被擦除成它们的 “原生”类型,即List。理解擦除以及应该如何处理它,是你在学习Java泛型时面临的最大障碍,这也是我们在本节将要探讨的内容。
1.2 List示例
当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以声明ArrayList.class,但是不能声明ArrayListcInteger>.class。请考虑下面的情况:
//: generics/ErasedTypeEquivalence.java
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
} /* Output:
true
*///:~ArrayListeString>和ArrayList<Integer>很容易被认为是不同的类型。不同的类型在行为方面肯定不同,例如,如果尝试着将一个Integer放入ArrayListeString>,所得到的行为(将失败)与把一个Integer放入ArrayListeInteger>(将成功)所得到的行为完全不同。但是上面的程序会认为它们是相同的类型。
边栏推荐
猜你喜欢

在 QT Creator 上配置 opencv 环境的一些认识和注意点

AutoJs学习-实现科赫雪花

AutoJs学习-AES加解密

【微信小程序2】事件绑定

【论文阅读】Distilling the Knowledge in a Neural Network

AutoJs学习-存款计算器

四字节的float比八字结的long范围大???

State Management in Jetpack Compose

PyQt5 (a) PyQt5 installation and configuration, read from the folder and display images, simulation to generate the sketch image

The packet capture tool Charles modifies the Response step
随机推荐
每天花2小时恶补腾讯T8纯手打688页SSM框架和Redis,成功上岸美团
nacos项目搭建
cococreator 动态设置精灵
EdrawMax Crack,多合一的图表应用程序
【Flink 问题】Flink 如何提交轻量jar包 依赖该如何存放 会遇到哪些问题
稳定币:对冲基金做空 Tether 的结局会是什么?
Bigder:41/100生产bug有哪些分类
百战RHCE(第四十六战:运维工程师必会技-Ansible学习1-基础知识讲解)
shell脚本
EPSANet: An Efficient Pyramid Split Attention Block on Convolutional Neural Network
RPA助你玩转抖音,开启电商运营新引擎
数据库mysql
uvm-phase机制
What is the function of page directive contentPage/pageEncoding in JSP page?
AutoJs学习-存款计算器
单词接龙 II
“蔚来杯“2022牛客暑期多校训练营4
边缘计算开源项目概述
被报表需求逼疯的银行数据人,是时候放弃用Excel做报表了
pnpm: Introduction