【effective java读书笔记】泛型(一)

来源:互联网 发布:js确认密码与密码验证 编辑:程序博客网 时间:2024/04/24 13:31

【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;    }@Overridepublic 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.Stringat 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 stubreturn 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.Stringat 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 stubreturn 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>() {@Overridepublic 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))方法。





















原创粉丝点击