《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,指向下一top
top = top.next;
return result;
}
}StackDemo
3 泛型接口
- 泛型也可以用于接口,例如生成器,生成器是专门负责创建对象类,一般只定义一个方法。
4 泛型方法
4.1泛型方法定义
- 泛型参数列表至于返回值前 如:
- public <T> void f(){}; 这是泛型方法;
- public <T, E> E 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 显式类型说明
- 泛型就是为了适用于多种类型,而显式类型说明却指定了泛型的具体类型
- 显式类型说明用在调用泛型方法上。
- 泛型方法调用后如果产生一个泛型结果,则不能将泛型结果传入另一个方法,而必须要这么做时就可以使用它显式类型说明。
在点操作符和方法名称之间插入<类型> 如果该泛型方法
1 和要传入方法(非静态方法)在同一个类要用 this.<>g()
2 是静态方法要用 类名.<>g()
- 3 通过对象调用
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;
}
[1, 2, 3, 4, juih]
并且可以准确分辨参数类型
System.out.println(f(1,2,3,4,"juih").get(3).getClass().getName());//java.lang.Integer
System.out.println(f(1,2,3,4,"juih").get(4).getClass().getName());//java.lang.String
4.5 生成器Generator思想
5 泛型用于匿名内部类
6 构建复杂模型
7 擦除
- jvm并不认识泛型因此需要将泛型擦除。
- ArrayList<String> 和 ArrayList<Integer>很容易被认为是不同类型。因为他们有不同的行为,但程序却认为他们是相同的,正是因为擦除的存在。
- 擦除的结果就是把一个对象变为它的原生类
- 泛型只是用来检查类型正确性,泛型部分的代码不会参与运行,这是由于泛型的擦除作用。
- 泛型代码内部无法获得有关泛型参数的类型的信息。
7.1 泛型擦除到第一个边界
- <T extends HasF > 上界 意思就是T 只能为HasF或者其子类。
- 泛型只是在静态类型检查期间出现来验证类型正确性,程序一但运行后泛型都将被擦除,替换成他们的非泛型上界,如List<T>被擦除为List,List<String>被擦除为List<Object>, <T extends HasF > 擦除为 <HasF>
7.2 擦除动机
- 擦除使得泛化的代码变得具体,因此泛化客户端可以使用非泛化类库,非泛化客户端也可以使用泛化类库。
7.3 擦除的代价
- 泛型不能当做一个类去操作,如Foo<Cat> 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<T> kind; 构造器传入类型参数赋值给kind,这样就得到了泛型的类型。
8.1 创建类型实例
- 创建泛型的实例 不可以 new T() 一来因为擦除,二来因为不能确定T是否有默认构造器.
- 利用类型标签 可以kind.newInstance()创建对象,但遇到没有默认构造器的类如Integer,运行时就会出错,而编译期无法捕获错误。
- Java解决方案是传入一个显示工厂对象来创建实例,并且限制其类型。
interface Factory<T>{ T create(); }
//创建显式工厂并限制类型为Integer
class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
return new Integer(0);
} } class Widget{
//创建显式工厂并限制类型为Integer
public static class FactionWidget implements Factory<Widget>{
@Override
public 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<T>来代替数组达到相同目的。
- 内部创建一个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<T extends Fruit> first = new ArrayList<T>(); 只能使用T
- List<? extends Fruit> first = new ArrayList<Apple>(); //可以使用各种Fruit的子类。
- List<? extends Fruit> 读作具有任何从Fruit继承的类型列表。
10.1 区别一个概念
- 数组可以向上转型,即导出类型的数组可以赋值给及基本类型数组的引用
- 而导出类型的泛型确不能赋值给基本类型泛型的引用 如: List<Fruit> list = new ArrayList<Apple>(); 语法错误
- 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>,表示持有Object对象的原生类,而List<?>是利用泛型给List持有的对象划了一个具体的范围,虽然范围也是Object,但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<B>(); 将a传入f(A<?> s)方法,f可以捕获确切类型 即s=A<B>
11 泛型存在的问题
- 基本数据类型无法作为泛型的类型参数,如<T> T不能是int 可以使用包装器类
- 自动包装机制不能用于数组int[ ] 不能成为Integer[ ]
- 带有泛型类型参数的转换或者使用instanceof判断类型都没有任何效果
- 被擦除后如果产生相同的方法签名那么不允许编译。
12 自限定的类型
13 动态类型安全
14 泛型中的异常
15 混型
16 潜在类型机制
17 对缺乏潜在类型机制的补偿
知识点:
《Java编程思想》笔记 第十五章 泛型的更多相关文章
- 《java编程思想》:第五章,初始化与清理
知识点整理: 1.从概念上讲,‘初始化’与‘创建’是彼此独立的,但是在Java中,两者被捆绑在一起,不可分离. 2.区分重载的方法:每个重载的方法都有一个独一无二的参数类型列表. 甚至参数顺序的不同也 ...
- java编程思想笔记(1)
java编程思想笔记(1) 一,对象的创建和生命周期 对象的数据位于何处?怎样控制对象的生命周期? 在堆(heap)的内存池中动态地创建对象. java完全采用了动态内存分配方式. 二,垃圾回收器 自 ...
- Java编程思想 笔记
date: 2019-09-06 15:10:00 updated: 2019-09-24 08:30:00 Java编程思想 笔记 1. 四类访问权限修饰词 \ 类内部 本包 子类 其他包 publ ...
- 《Linux命令行与shell脚本编程大全》 第十五章 学习笔记
第十五章:控制脚本 处理信号 重温Linux信号 信号 名称 描述 1 HUP 挂起 2 INT 中断 3 QUIT 结束运行 9 KILL 无条件终止 11 SEGV 段错误 15 TERM 尽可能 ...
- #Java编程思想笔记(一)——static
Java编程思想笔记(一)--static 看<Java编程思想>已经有一段时间了,一直以来都把笔记做在印象笔记上,今天开始写博客来记录. 第一篇笔记来写static关键字. static ...
- 2.1(java编程思想笔记)位移操作
java位移操作主要有两种: 有符号位移:有符号位移会保留原有数字正负性,即正数依然是正数,负数依然是负数. 有符号位左移时,低位补0. 有符号右移时:当数字为正数,高位补0.当数字为负时高位补1. ...
- 【学习笔记】《Java编程思想》 第1~7章
第一章 对象导论 对整书的概要. 略读. 第二章 一切都是对象 创建一个引用,指向一个对象. 安全的做法:创建一个引用的同时便进行初始化. 对象存储的地方:1)寄存器:这是最快的存储区,因为它位于不同 ...
- 《Java并发编程实战》第十五章 原子变量与非堵塞同步机制 读书笔记
一.锁的劣势 锁定后假设未释放.再次请求锁时会造成堵塞.多线程调度通常遇到堵塞会进行上下文切换,造成很多其它的开销. 在挂起与恢复线程等过程中存在着非常大的开销,而且通常存在着较长时间的中断. 锁可能 ...
- java编程思想笔记(第一章)
Alan Kay 第一个定义了面向对象的语言 1.万物皆对象 2.程序是对象的集合,他们彼此通过发送消息来调用对方. 3.每个对象都拥有由其他对象所构成的存储 4.每个对象都拥有其类型(TYpe) 5 ...
随机推荐
- 关于如何利用原生js动态给一个空对象添加属性以及属性值
首先,回忆一下,访问对象属性一共有两种方法:点获取法和方括号获取法.而我们最常用的就是点获取法了.但是当我们遇到需要给对象动态添加属性和属性值时,点获取法好像就不太好用了,尤其是我们不知道属性名的时候 ...
- exit和die的区别
网上搜索die与exit两个函数的区别,大部分的"标准答案"都是说die是退出并释放内存,exit是退出但不释放内存. 这个解释显然是错的,PHP手册中已经说过"die ...
- Java IO 之 System类
1.使用System.in.read读取,使用System.out.println 输出 package org.zln.io; import java.io.IOException; /** * C ...
- P1118 [USACO06FEB]数字三角形`Backward Digit Su`…
题目描述 FJ and his cows enjoy playing a mental game. They write down the numbers from 11 to N(1 \le N \ ...
- Notice : brew install php70
To enable PHP in Apache add the following to httpd.conf and restart Apache: LoadModule php7_module ...
- wmic的用法
原始文章链接:http://blog.sina.com.cn/s/blog_5fb265c70100w4d0.html 一.wmic的基本命令格式简析 经常看网上的相关资料的话,读者可能会对wmic有 ...
- dhcp 和ntpdate时间同步
为了防止路由器的dhcp服务干扰实验,我们2台机器分别新加了1快网卡. vmnet4 dhcp安装 [root@ygy130 ~]# yum -y install dhcp 将配置文件放在/etc/d ...
- 近期对于windows服务的理解
1.APP.config的作用 在开发环境下时,根目录下的APP.config里面会填写一些参数之类的.当生成之后,这些参数将会被自动生成在*.exe文件目录中.如图: 其中,.exe文件为Win ...
- 有关spring的各种下载资料的网站
spring的文件和jar包下载的网站: https://repo.spring.io/release/org/springframework/spring/ spring 各个版本源码下载的资料: ...
- bzoj5091 [Lydsy1711月赛]摘苹果 概率题
[Lydsy1711月赛]摘苹果 Time Limit: 1 Sec Memory Limit: 256 MBSubmit: 174 Solved: 135[Submit][Status][Dis ...