【effective java读书笔记】泛型
一、泛型擦除的概念
例:代码一:
package com.generic;
public class Pair<U, V> {
U first;
V second;
public Pair(U first, V second){
this.first = first;
this.second = second;
}
public U getFirst() {
return first;
}
public V getSecond() {
return second;
}
@Override
public String toString() {
return "Pair [first=" + first + ", second=" + second + "]";
}
}
代码二:
package com.generic;
public class GenericDemo {
public static void main(String[] args) {
Pair<Integer,String> kv = new Pair<Integer,String>(1,"bobo");
System.out.println(kv.toString());
}
}
javac编译后,阅读.class文件如下:编译器将泛型(Pair<Integer,String>)编译后转换成原生态类型(本例中Pair即原生态类型)。jvm执行的时候并没有泛型的概念,泛型已经被擦除。
package com.generic;
import java.io.PrintStream;
public class GenericDemo
{
public static void main(String[] paramArrayOfString)
{
Pair localPair = new Pair(Integer.valueOf(1), "bobo");
System.out.println(localPair.toString());
}
}
ok,理解了擦除的概念,那么问题就来了,
<1>既然编译器最终得到的是原生态类型,那么我们为什么不直接就用原生态类型?帮编译器省事些不更好么?
泛型的作用一:可以告诉编译器每个集合中接受哪些类型,使程序更加安全清楚。编译时即可发现错误。将运行时错误提前到编译时发现并处理。
<2>什么情况必须用原生态类型而不能用泛型?需要使用类文字的时候,例如DataBean.class。反射时多用到类似用法。
总之,除了以上必须使用原生态类型的时候,都使用泛型就好。
二、列表优先数组:(数组是协变的)
协变(这个词汇就理解为数组的一个特性吧,虽然这个特性不知道能做什么)。
例:在一个Long数组中添加了一个String对象;编译不报错!(如下第一行代码就是协变,与向上转型区分开)
Object[] objs = new Long[1];
objs[0] = "i am string";
System.out.println(objs[0]);
运行时提示:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at com.generic.GenericDemo.main(GenericDemo.java:17)
当然我们可以让它在编译时就报错:
Long[] objs = new Long[1];
objs[0] = "i am string";
System.out.println(objs[0]);
第2行就提示错误:cannot convert string to long例:在向上转型的容器类中,编译时就提示错误。
List<Long> objstrs = new ArrayList<Long>();
objstrs.add("i am strings");
三、类泛型改造
用原生态类型类写一个栈:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY=16;
public Stack(){
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
System.out.println("栈的大小"+size);
}
private void ensureCapacity() {
// TOD确保大小自增
if (elements.length==size) {
elements = Arrays.copyOf(elements, 2*size+1);
System.out.println("栈的长度"+elements.length);
}
}
public Object pop(){
if(isEmpty()){
throw new EmptyStackException();
}
Object result = elements[--size];
System.out.println("栈的大小"+size);
//弹出的元素置空
elements[size] = null;
return result;
}
public boolean isEmpty() {
// TODO Auto-generated method stub
return size==0;
}
}
说明:
使用Object数组。push任何类型都可以。并且可以混用。混用举例如下。
public static void main(String[] args) {
Stack stack = new Stack();
String str2 = "bobo";
//压入字符串
stack.push(str2);
//压入数值
stack.push(10010);
while (!stack.isEmpty()) {
String strpop = (String) stack.pop();
//将栈中弹出数据大小写转换
System.out.println(strpop.toUpperCase());
}
}
这种用法push进去是完全没有问题的。编译也不会报错。但是当pop出来的时候,如果不做任何处理的pop也不会有错误。
但是一般来说,我们都会对它进行处理。比如类型转换。由于没有约束,很可能就给其他使用者一种兼容所有类型的假象。然而运行时,却会提示错误如下:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.generic.TestStack.main(TestStack.java:14)
当然,我们确实可以去兼容它(通过instance of)。但是实际生活中我们很少去这样做。例如:
public static void main(String[] args) {
Stack stack = new Stack();
String str2 = "bobo";
//压入字符串
stack.push(str2);
//压入数值
stack.push(10010);
while (!stack.isEmpty()) {
Object obj = stack.pop();
//对弹出对象判断所属是否字符串
if (obj instanceof String) {
//将栈中弹出数据大小写转换
System.out.println(((String)obj).toUpperCase());
}
//对弹出对象判断所属是否整数
if(obj instanceof Integer){
System.out.println(obj);
}
}
}
我们更常用的是对stack栈进行泛型约束:这样做就人性化许多,编译时报多少错误都无所谓,只要改了就好。但是运行时错误,如果一旦发生,可能就是线上用户反馈而来的。可想而知。孰轻孰重。
public static void main(String[] args) {
Stack2<String> stk = new Stack2<>();
stk.push("haibobo");
//编译时就提示不允许压入其他类型,此处编译错误
stk.push(10001);
while (!stk.isEmpty()) {
System.out.println(stk.pop().toUpperCase());
}
}
对栈的泛型改造如下:(基于上一版本各种注解即改造的位置,不再赘叙)
//添加类的泛型约束
public class Stack2<E> {
//泛型定义类的数组
private E[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY=16;
@SuppressWarnings("unchecked")
public Stack2(){
//此处由于数组必须是具体的某种类型,需要强转
elements = (E[]) new Object[DEFAULT_CAPACITY];
}
//push压入泛型类的元素
public void push(E e){
ensureCapacity();
elements[size++] = e;
System.out.println("栈的大小"+size);
}
private void ensureCapacity() {
// TOD确保大小自增
if (elements.length==size) {
elements = Arrays.copyOf(elements, 2*size+1);
System.out.println("栈的长度"+elements.length);
}
}
//弹出泛型的结果集
public E pop(){
if(isEmpty()){
throw new EmptyStackException();
}
E result = elements[--size];
System.out.println("栈的大小"+size);
//弹出的元素置空
elements[size] = null;
return result;
}
public boolean isEmpty() {
// TODO Auto-generated method stub
return size==0;
}
}
四、泛型方法改造
原生态类型的方法如下:此方法的作用:将两个Set集合的内容整合成一个。当然这个方法,在如今的开发环境下,例如eclipse下都会有警告,也就是通常意义上讲的编译警告。
public static Set union(Set s1,Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
至于警告的原因,此处我的理解应该还是因为没有约束容易导致多种类型联合的警告。例如:
public static void main(String[] args) {
Set<String> strs1 = new HashSet<>(Arrays.asList("Tom","Dick","Harry"));
Set<Integer> strs2 = new HashSet<>(Arrays.asList(1001,1002,1003));
Set result = union(strs1, strs2);
System.out.println(result);
}
得到结果如下:
[Tom, Harry, 1001, 1002, 1003, Dick]
这样子不加约束的放在一起,放进去容易,想要取出来用的时候,肯定是要操碎心的,毕竟各种类型的数据,你需要做各种类型的处理,但,如果你是给别人提供方法,你觉得别人真的能理解你么?背锅侠还是少做好。还是通过约束分开吧。改造后方法如下:
public static <E> Set<E> union(Set<E> s1,Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
这样子,任何人使用的时候,都会遵循约束条件了:你把你的约束交给了编译器,谁敢不服?
public static void main(String[] args) {
Set<String> strs1 = new HashSet<>(Arrays.asList("Tom","Dick","Harry"));
Set<Integer> strs2 = new HashSet<>(Arrays.asList(1111,2222,3333));
//这么使用,编译器提示错误
Set<String> result = union(strs1, strs2);
System.out.println(result);
}
来,只能这么用:
public static void main(String[] args) {
Set<String> strs1 = new HashSet<>(Arrays.asList("Tom","Dick","Harry"));
Set<Integer> strs2 = new HashSet<>(Arrays.asList(1111,2222,3333));
//这么使用,编译器提示错误
Set<String> result = union(strs1, strs2);
System.out.println(result);
}
告诉他,结果集也是全部都是字符串结果集,放心大胆的用吧!
[Moe, Tom, Harry, Larry, Curly, Dick]
五、泛型接口的改造
查看一个普通的接口。这样一个接口自身也是无任何问题的。那么问题在哪呢?也就是如果是一个针对各种类型的处理,相同逻辑的接口,那么接口必须写多个。例如:string如下,Integer肯定也得新写,这就失去了写接口的初衷了。
public interface UnaryFunction<String> {
String apply(String arg);
}
代码一:改造如下:
public interface UnaryFunction<T> {
T apply(T arg);
}
代码二:然后写一个单例模式对象:
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
@Override
public Object apply(Object arg) {
return arg;
}
};
代码三:返回获得这个单例对象的方法(由于单例对象是通过Object实现的,通过它接口的类型对它进行转型):
public static <T> UnaryFunction<T> identityFunction(){
return (UnaryFunction<T>) IDENTITY_FUNCTION;
}
使用这个单例模式获取对象主方法如下:
public static void main(String[] args) {
String[] strings = {"jute","hemp","nylon"};
UnaryFunction<String> sameString = identityFunction();
for (String s:strings) {
System.out.println(sameString.apply(s));
}
}
第3行代码可见:调用代码三的identityFunction方法,返回参数String,根据类型推导,得到接口UnaryFunction即代码一也同时约束为String。其中apply方法也受到String约束,例如本例改为sameString.apply(1001);则会提示整形不能cast为String类型。
总结为也就是一个类型推导的概念。比如该接口使用泛型,并没有传递进一个String参数,但是通过返回值UnaryFunction<String>对应接口中
(UnaryFunction<T>) IDENTITY_FUNCTION,推导出其中参数为String。然后对整个接口T即为String类型,通过String类型约束其他(包括apply(T arg))方法。