Java编程思想 第十五章:泛型
1. 泛型
- “泛型”意思就是适用于许多类型。
- 使用泛型的目的之一: 指定容器持有什么类型,让编译器确保正确性,而不是在运行期发现错误。
- 这个容器可以看成是有其他类型对象作为成员的类,而不单单只是JDK中的容器类。
2.简单的泛型
2.1 元组
- 元组是对象,是一个将多个对象打包存储于其中的单一对象。Java中没有元组这个类库,Python中有元组这一概念。
- 可以通过泛型构建自己的元组类库。
class TwoTuple<A,B>{public final A first;public final B second;TwoTuple(A a, B b){first = a;second = b;}
- 元组允许读取元素,但不能插入新元素,不可以修改元素值,因为元素被设置为final。
- 元组可以任意长度,可以存储任何类型对象。
2.2 元组泛型的继承
- 父类的泛型同样可以继承给子类,但要显示的写出父类的泛型
class ThreeTuple<A,B,C> extends TwoTuple<A,B>{public final C three;public ThreeTuple(A a, B b, C c){super(a,b);three = c;}
- 一个方法只能返回一个对象,但返回一个元组就可以包含多个对象。
public static TwoTuple<String,Integer> f(){return new TwoTuple<String, Integer>("hi",99);}
2.3 模拟堆栈
- 用泛型实现一个自定义堆栈
- 使用内部链式存储机制
public class StackDemo {public static void main(String[] args) {LinkedStack<String> lss = new LinkedStack<String>();lss.push("please");lss.push("say");lss.push("good");String s;while ((s = lss.pop()) != null)System.out.println(s);}
}class LinkedStack<T> {//定义栈结点,结点为一个对象//结点保存元素类型使用泛型45表示private class Node<U> {U item; //要保存的数据Node<U> next; //指向下一结点的引用Node() { //默认构造器,构造空结点item = null;next = null;}//该构造器给结点赋值Node(U item, Node<U> next) {this.next = next;this.item = item;}//判断栈是否为空boolean end() {return item == null && next == null;}}//设置一个末端哨兵,该哨兵结点为空结点private Node<T> top = new Node<T>();//压栈 每次push创建一个Node结点public void push(T item) {/*第一次压栈将末端哨兵连接到该结点的next,并且top不再是末端哨兵而是第一个结点第二次压栈将第一个结点连接在第二个结点的next上,top是第二个结点...以此类推 整个栈完成*/top = new Node<T>(item, top);}//弹栈public T pop() {T result = top.item; //得到结点数据if (!top.end()) //如果本结点不是空元素,则丢弃当前top,指向下一toptop = top.next;return result;}
}StackDemo
3.泛型接口
泛型也可以用于接口,例如生成器,生成器是专门负责创建对象类,一般只定义一个方法。
4.泛型方法
4.1 泛型方法定义
泛型参数列表至于返回值前 如:
- public void f(){}; 这是泛型方法;
- public int f(T a){};这不是泛型方法,返回值前无泛型。
4.2 泛型方法
泛型方法所在的类可以是泛型类,也可以不是泛型类,并且泛型标识符可以完全不一样,也就是说泛型方法和泛型类无关。
普通static方法无法访问泛型类的类型参数,如果要是使用泛型就要定义成泛型静态方法
public class Gy<T> {T name;/* public static T f(T a){ //编译不通过return a;}*/public static <T> T f(T a){ return a;}public T g(T b){return b;}}
类的泛型要在创建对象时才确定,而类内的静态方法,静态域在类加载时初始化,因此如果使用类的泛型类型则初始化时无法知道具体类型是什么,此时使用泛型方法这样就和类的泛型无关了,这样静态方法初始化时类型只和自身的泛型相关。
- 使用泛型方法时编译期会通过类型参数推断来为我们找出具体类型,而不必自己声明时什么类型。
- 泛型方法返回值是泛型,那么就返回一个泛型,不能是具体类型,反之亦然。
public static <T> Set<T> set(){// return new HashSet<String>(); //不能返回具体类型}
4.3 显式类型说明
- 泛型就是为了适用于多种类型,而显式类型说明却指定了泛型的具体类型
- 显式类型说明用在调用泛型方法上。
- 泛型方法调用后如果产生一个泛型结果,则不能将泛型结果传入另一个方法,而必须要这么做时就可以使用它显式类型说明。
- 在点操作符和方法名称之间插入<类型> 如果该泛型方法
- 和要传入方法(非静态方法)在同一个类要用 this.<>g()
- 是静态方法要用 类名.<>g()
- 通过对象调用
public static <T> Set<T> set(){return new HashSet<T>();}public static void f(Set<List> stringSet){}public static void main(String[] args) {f(Gy.<List>set());}
4.4 可变参数泛型方法
public static void main(String[] args) {System.out.println(f(1,2,3,4,"juih"));}public static <T> List<T> f(T... args){List<T> result = new ArrayList<T>();for (T item : args) {result.add(item);}return result;}
4.5 生成器Generator思想
5.泛型用于匿名内部类
泛型还可以应用于内部类以及匿名内部类,下面的示例使用匿名内部类实现了Generator接口。
import java.util.*;
import net.mindview.util.*;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(); }};}
}
6.构建复杂模型
7. 擦除
- jvm并不认识泛型因此需要将泛型擦除。
- ArrayList 和 ArrayList很容易被认为是不同类型。因为他们有不同的行为,但程序却认为他们是相同的,正是因为擦除的存在。
- 擦除的结果就是把一个对象变为它的原生类
- 泛型只是用来检查类型正确性,泛型部分的代码不会参与运行,这是由于泛型的擦除作用。
- 泛型代码内部无法获得有关泛型参数的类型的信息。
7.1 泛型擦除到第一个边界
- 上界 意思就是T 只能为HasF或者其子类。
- 泛型只是在静态类型检查期间出现来验证类型正确性,程序一但运行后泛型都将被擦除,替换成他们的非泛型上界,如List被擦除为List,List被擦除为List, 擦除为
7.2 擦除动机
擦除使得泛化的代码变得具体,因此泛化客户端可以使用非泛化类库,非泛化客户端也可以使用泛化类库。
7.3 擦除的代价
泛型不能当做一个类去操作,如Foo cat不能代表Cat这个类,因为它会被擦除为Object.
7.4 边界处的动作
泛型中创建数:Array.newInstance(类<?> componentType, int length) 并且要强制转型为T[]类型。
public class Test<T> {private Class<T> kind;T[] create(int size){return (T[]) Array.newInstance(kind,size);//必须强转T[]}List<T> createList(){return new ArrayList<T>();}
}
- 边界就是对象进入和离开方法的地方,编译期执行类型检查和插入转型代码就是在边界处。
- 编译期执行了类型检查确保了数据一致性,在离开方法时由编译器为你插入转型代码执行转型,此时转型是安全的。
- 由于擦除kind实际被存储为Class,因此创建数组无法后知道要转型成什么类型,因此必须强转。但创建容器就不需要强转了,编译期可以保证类型的一致性,如果类型不一致不通过编译。
8.擦除补偿
由于擦除存在,所以任何在 运行时 需要知道泛型代表的类型的 确切类型信息 的操作都无法工作。
解决办法:引入类型标签
- 给泛型类添加一个标签成员Class kind; 构造器传入类型参数赋值给kind,这样就得到了泛型的类型。
8.1 创建类型实例
- 创建泛型的实例 不可以 new T() 一来因为擦除,二来因为不能确定T是否有默认构造器.
如果需要创建类型实例,就需要如下方式:
- 利用类型标签 可以kind.newInstance()创建对象,但遇到没有默认构造器的类如Integer,运行时就会出错,而编译期无法捕获错误。
- Java解决方案是传入一个显示工厂对象来创建实例,并且限制其类型。
interface Factory<T>{T create();}
//创建显式工厂并限制类型为Integer
class IntegerFactory implements Factory<Integer>{@Overridepublic Integer create() {return new Integer(0);}}class Widget{//创建显式工厂并限制类型为Integerpublic static class FactionWidget implements Factory<Widget>{@Overridepublic Widget create() {return new Widget();}}
}
class Foo2<T> {private T x;public <F extends Factory<T>> Foo2(F factory){x = factory.create();}}
public class Test{public static void main(String[] args) {// 创建Foo2对象实例 并且x为泛型的实例对象new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.FactionWidget());}
}
- 使用模板方法设计模式
8.2 泛型数组
不能直接创建泛型数组 T[] array = new T[size]
可以定义一个泛型数组的引用 T[ ] array ,但无法使用这个引用。
解决办法
- 可以使用ArrayList来代替数组达到相同目的。
- 内部创建一个Object[ ] 数组,在需要时将数组内的对象转型为需要的类型,但不能将Object[ ]转型为T[ ],因为没有任何方式可以改变数组底层类型。
public class Test<T> {private Object[] array={"ji"};public T get(int index) {return (T) array[index];}public static void main(String[] args) {Test<String> test = new Test<String>();String s = test.get(0);// String s = (String)test.get(0);编译器自动插入转型代码}}
其实 get内没有任何类型转换 ,T 被擦除成了Object,只是Object转Object了, 创建对象确定T类型后在编译阶段编译器会为你插入转型代码。
9. 边界
- 作用:
- 强制泛型可以使用什么类型
- 按边界类型调用方法其方法,无边界的只能调用从Objec继承的方法。
9.1 多边界
- <T extends A & B & C > A B C 之间没有继承关系
- 多边界擦除到第一个边界A。
10. 通配符
- 通配符可以允许某种类型向上转型,与普通边界对比:
- List first = new ArrayList(); 只能使用T
- List<? extends Fruit> first = new ArrayList(); //可以使用各种Fruit的子类。
- List<? extends Fruit> 读作具有任何从Fruit继承的类型列表。
10.1 区别一个概念
- 数组可以向上转型,即导出类型的数组可以赋值给及基本类型数组的引用
- 而导出类型的泛型确不能赋值给基本类型泛型的引用 如: List list = new ArrayList(); 语法错误
- Apple 是 Fruit 的子类,但 Apple所在的List却不是Fruit所在的List的子类,故不能这样转型。
10.2 上界和下界
<? extends Fruit> 上界 ?是Fruit 的子类,但具体是什么不知道,因此当调用get方法时返回的对象可以赋值给Fruit引用,而add添加对象时由于不清楚具体要添加什么子类所以无法使用add方法。 <? super Apple > 下界 也称 逆变?是Apple的父类,但具体是什么类型不得而知,因此当调用add方法添加对象时可以添加Apple和其子类对象,但调用get方法时无法确定要返回什么类型,因此不能调用get方法返回具体类型,只能返回Object。 ## 10.3 无界通配符 <?>
表示任何类型,相当于使用原生类 ,他还意味着声明 “我要在这里使用泛型”
原生类List实际上是List,表示持有Object对象的原生类,而List<?>是利用泛型给List持有的对象划了一个具体的范围,虽然范围也是Object,但List<?>确不是原生类。
下面我们来看一下List<?>和看起来等价于List之间的差异?
因为,事实上,由干泛型参数将擦除到它的第一个边界,因此List<?>看起来等价于List,而List实际上也是List——除非这些语句都不为真。List实际上表示“持有任何Object类型的原生List”,而List<?>表示“具有某种特定类型的非原生List,只是我们不知道那种类型是什么。”
10.4 <?>与上下界之间的区别
- 一个方法的参数的类型如是 List ,List<?> ,则可以接收任何形式的List参数,参数是不是泛型无所谓。
- 参数的类型如果是List<? extends/super A > ,则只能接收泛型的List参数.
- 如果参数的类型是 <?> 或者 <? extends A>,则该方法无法调用
- <?>可以向上转型
- 多个泛型参数下只有全为?时编译器无法与原生类区分,但只要有过一个参数不是?就会有所区分如Map<String, ?>必须传入map<String,?>类型的参数,而Map<?,?>可以传入new HashMap();
10.5 捕获转换
向一个<?>方法传入有个原生类型,编译器可以捕获实际类型参数,这个<?>方法调用其他方法时向其他方法传递被捕获的对象时就会传递确切类型。如 A a =new A(); 将a传入f(A<?> s)方法,f可以捕获确切类型 即s=A
11.泛型存在的问题
- 基本数据类型无法作为泛型的类型参数,如 T不能是int 可以使用包装器类
- 自动包装机制不能用于数组int[ ] 不能成为Integer[ ]
- 带有泛型类型参数的转换或者使用instanceof判断类型都没有任何效果
- 被擦除后如果产生相同的方法签名那么不允许编译。
12.自限定的类型
13.动态类型安全
14.泛型中的异常
15.混型
16.潜在类型机制
17.对缺乏潜在类型机制的补偿
Java编程思想 第十五章:泛型相关推荐
- 【JAVA SE】第十五章 ArrayList、LinkedList、HashMap和HashSet
第十五章 ArrayList.LinkedList.HashSet和HashMap 文章目录 第十五章 ArrayList.LinkedList.HashSet和HashMap 一.ArrayList ...
- JAVA编程思想学习笔记——第一章 对象导论
搞了一年多java,野路子出身,发现java基础这块还是相当的薄弱!故决定学习<Java编程思想>这本书.在此把学习的知识点记录下! 面向对象的五大特性 1.万物皆为对象 2.程序是对象的 ...
- Java编程思想—第十二十三章
1.在堆中new出异常对象 2.通常用System.err来输出异常,而System.out可能会被重定向,不建议用. 3.在catch中抛出异常throw e.则之后的catch子句将被忽略. 4. ...
- Java编程思想学习-《第二章 一切都是对象》
第2章 一切都是对象 尽管Java是基于C++的,但是相比之下,Java是一种更"纯粹"的面向对象程序设计语言.Java语言假设我们只进行面向对象的程序设计.也就是说,在开始用Ja ...
- Java编程思想读书笔记——第九章:接口
第九章 接口 接口和实现类 抽象类是介于普通的类和接口之间的中庸之道,抽象类也是一种重要的工具,你不可能总是使用纯接口 9.1 抽象类和抽象方法 抽象方法声明的语法: abstract void f( ...
- Java编程思想 第十八章 Java I/O系统
文章目录 18.1 File类 18.1.1 获取当前目录下文件名并做过滤 18.1.2 递归获取指定目录下文件集并过滤 18.1.3 目录的检查及创建 18.2 输入和输出 18.2.1 Input ...
- easyloging 获取日志文件名字_愉快地学Java语言:第十五章 断言与日志
导读 本文适合Java入门,不太适合Java中高级软件工程师.本文以<Java核心技术基础知识卷I>第10版为蓝本,采用不断提出问题,然后解答问题的方式来讲述.本篇文章只是这个系列中的一篇 ...
- Java编程思想—第八九章
1.前期绑定 后期绑定 当子类向上转型为基类对象的时候,不知道原来是哪一个子对象,所以需要辨别,此时用到了绑定,来判定哪一个子对象. 前期绑定:运行程序之前就绑定. 后期绑定:Java中除了stati ...
- 《java编程思想》第七章 复用类
组合:在新的类中产生现有类的对象.只是复用了现有程序代码的功能,而非形式. 继承:按照现有类的类型来创建新类,无需改变现有类的形式.采用现有类(基类)的形式,并在其中添加新代码. 都是利用现有类型生成 ...
最新文章
- 一个 NAND flash写函数
- 【iOS XMPP】使用XMPPFramewok(三):好友状态
- win2008的搜索功能就是个鸡肋
- 【Python】Pycharm中plot绘图不能显示
- Playing Atari with Deep Reinforcement Learning 中文 讲解
- TUXEDO中间件介绍及应用
- Linux与网络基础知识
- java面试题笔试常见选择题大全含答案
- android app消息推送,如何进行app消息推送(push)?
- 在拉勾网对职位的数据爬取与分析
- 盯上年轻人的今日头条,重新以内容出发还有多少可能?
- 最新视频连接解析地址
- 在OpenCV里实现伽马变换
- 海天蚝油《挑战不可能》实测5G超强传输能力
- 科研—画图图片处理1
- 使用StreamTorrent观看流媒体电视
- Intel Xeon E5-4650 VS AMD Opteron 6380
- postgresql数据库字节流类型详解
- Android自动化的一般方法
- 电脑通过手机上网的方法
热门文章
- 缓存 Memached
- c++字符前面的L和_T
- mysql 语法积累
- 配置Win Server 2008 R2 防火墙允许远程访问SQL Server 2008 R2
- songsoft-关于加薪的策略
- 76.数据库操纵语言DML 定义语言 DDL 控制语言DCL
- 6.排序算法最优的时间复杂度
- 霍金的预言正在实现,我们已经离不开人工智能,而它们在脱离控制
- 人工智能时代下的“烦恼”:美国国会探讨“深度伪造”风险及对策
- 苹果证实收购Drive.ai自动驾驶汽车初创公司