当每一章阅读的时候,首先这个概念为什么会出现,有什么用,而根据这个概念的运用,为什么这么用,有什么好处。我觉得这个对理解概念的东西会事半功倍。
目录
匿名内部类(可以在匿名内部类中使用泛型)、构建复杂模型(其实也就是多维元组)
简单泛型(基本都是介绍设计代码的用法)
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
}
}
上面例子使用了泛型<T>可以返回任意一个对象,这就是一个简单泛型的使用。这样带给我们什么好处呢,假设我们有一个苹果类跟一个毛巾类,而我们需要做一个洗东西的类,这个时候可以将苹果对象跟毛巾对象传入洗类中,苹果跟毛巾是两种不同的东西,那么就没办法使用多态了,这个时候就需要写一个洗毛巾跟一个洗苹果的类,显得过于繁琐,使用泛型就可以做一个公用的类传入苹果或者是毛巾了,显得代码更加简洁。
元组类库(就是一个对象持有多个对象)
元组(又称为数据传送对象或者是信使),并且进行读取并且在进行初始化操作之后不能添加新的对象。下面是一个三维元组继承一个二维元组,这样final定义了C类是初始化之后无法修改,也可以使用private进行修饰。如果你想别人使用这个类可以直接调用C作为赋值就可以使用public final进行修饰,final会保护数据不会被修改。
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 +")";
}
}
一个堆栈类(其实是泛型的一个使用,做一个类型堆栈的,也就是像堆砖头一样取对象是后放的先出)
public class LinkedStack<T> {
private static class Node<U> {
U item;
Node<U> next;
Node() { item = null; next = null; }
Node(U item, Node<U> next) {
this.item = item;
this.next = next;
}
boolean end() { return item == null && next == null; }
}
private Node<T> top = new Node<T>(); // End sentinel
public void push(T item) {
top = new Node<T>(item, top);
}
public T pop() {
T result = top.item;
if(!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<String>();
for(String s : "Phasers on stun!".split(" "))
lss.push(s);
String s;
while((s = lss.pop()) != null)
System.out.println(s);
}
} /* Output:
stun!
on
Phasers
*///:~
书中这个例子就是,把Node通过pop()赋值给自己,然后默认item与next为null,而end()作为一个哨兵机制判断是否为null,然后取Node对象时,将最上层对象赋值给top,一直循环直到end()判断为null,就可以作为一个栈堆类来使用。
RandomList(一个工具类,返回List中随机的某一个元素,同样也可以改造成Map、Set,下面例子理解一下就可以明白了)
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList<T>();
private Random rand = new Random(47);
public RandomList(List<T> list){storage=list;}
public void add(T item) { storage.add(item); }
public T select() {
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<String>();
for(String s: ("The quick brown fox jumped over " +
"the lazy brown dog").split(" "))
rs.add(s);
for(int i = 0; i < 11; i++)
System.out.print(rs.select() + " ");
}
RandomList<String> rs = new RandomList<String>(rs);
for(int i = 0; i < 11; i++)
System.out.print(rs.select() + " ");
}
}
泛型接口
简单讲解下下面例子,Gennerator是一个生成器接口,而使用泛型<T>成为了一个泛型接口,这个时候,生成器时可以复用的,CoffeeGenerator可以返回Coffee,假设我还有一个Tea类也去实现这个Gennerator生成器,而在我们进行调用时,可以根据多态调用到我们传入的Coffee或者是Tea,生成对应的Coffee或者是Tea对象。假如我们不使用泛型接口,那么我们要做到复用生成器,首先需要编写Gennerator抽象类,继承,但是这样的话,没有办法多重继承,可能会让我们的代码更加复杂。
public interface Generator<T> { T next(); } ///:~
public class CoffeeGenerator
implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = { Latte.class, Mocha.class,
Cappuccino.class, Americano.class, Breve.class, };
private static Random rand = new Random(47);
public CoffeeGenerator() {}
// For iteration:
private int size = 0;
public CoffeeGenerator(int sz) { size = sz; }
public Coffee next() {
try {
return (Coffee)
types[rand.nextInt(types.length)].newInstance();
// Report programmer errors at run time:
} catch(Exception e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
public boolean hasNext() { return count > 0; }
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i < 5; i++)
System.out.println(gen.next());
for(Coffee c : new CoffeeGenerator(5))
System.out.println(c);
}
}
泛型方法
首先对于泛型方法的使用,在返回类型前加上泛型,如果不加,当T作为参数编译器会报错
public <T> T f(T x) {
return x;
}
同时泛型方法也可以跟可变参数共存
public <T> void f(T... x) {
//code
}
泛型方法可以自动做“类型参数推断(type argument inference)”,也就是说当你传入一个参数,如果是String,会自动将参数转换为String。但是这里需要注意的是,当执行String result = f("String");类型参数推断会自动将返回值转换为String。但是看下面例子
public void show(String a){}
执行show( f("String"));时会编译器报错,因为当你返回时作为参数,因为没有赋值,所以赋值给Object变量,导致编译会出问题。这个时候就需要转换,show( <String>f("String"))在调用前加入泛型说明。
类型参数推断有什么用呢,看下面,类型参数推断可以在赋值是自动转化省去了一些功夫,但是假设别人需要去使用New类,也需要时间去理解代码的含义,所以“很讽刺”,使用本意是让代码看起来通俗易懂,假设这个New很复杂,需要去花大量的时间去理解含义,所以“很难说它为我们带来了多少好处”,这也是杠杆利用类型参数判断,根据场景权衡。
public class New {
public static <K,V> Map<K,V> map() {
return new HashMap<K,V>();
}
public static <T> List<T> list() {
return new ArrayList<T>();
}
public static <T> LinkedList<T> lList() {
return new LinkedList<T>();
}
public static <T> Set<T> set() {
return new HashSet<T>();
}
public static <T> Queue<T> queue() {
return new LinkedList<T>();
}
// Examples:
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
List<String> ls = New.list();
LinkedList<String> lls = New.lList();
Set<String> ss = New.set();
Queue<String> qs = New.queue();
}
}
书中还有很多用法,这里不列举了,我们需要了解的泛型方法的意义,使用方法以及类型推断能够给我们带来的好处,就不必强记一些用法了,想透自然会懂。
匿名内部类(可以在匿名内部类中使用泛型)、构建复杂模型(其实也就是多维元组)
擦除(这才是我们应该需要去记住的东西)
何为擦除,看下面,c1和c2应该是不相同的两种类型
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
但是,输出结果发生结果为true,这就奇怪了
System.out.println(c1 == c2);//true
而得出的结论:,虽然看起来两个List操作起来会不同,但是真正代码上,其实泛型擦除了ArrayList<String>()和ArrayList<Integer>()的String、Integer成为了默认的List类型,而不是两种不同的类型。其实用通俗点的话就是,使用泛型是让编译器去规范传入的类型参数,有问题报错而不是在启动程序之后才报错。下面具体理解擦除和如何去处理。
首先,擦除给我们带来什么影响。
public class HasF {
public void f() { System.out.println("HasF.f()"); }
}
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Error: cannot find symbol: method f():
// public void manipulate() { obj.f(); }
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator =
new Manipulator<HasF>(hf);
manipulator.manipulate();
}
}
注释的manipulate()方法调用obj.f()方法是不通过的,这是什么原因。尽管编译器知道有HasF有f()方法,照理来说它应该可以进行匹配把f()映射到obj.f()上可以编译通过,但是由于泛型的擦除导致了这个参数变为了Object,而c++是可以的,但JAVA又是受到了C语言的启发,所以这相对于C++甚至是C#来说是有缺陷的,这也会导致我们可能在做一些操作上需要添加上一个边界来让编译器去进行映射,如下通过extends来规定一个范围,一个边界,编译器才能去进行映射。
class Manipulator2<T extends HasF> {
private T obj;
public Manipulator2(T x) { obj = x; }
public void manipulate() { obj.f(); }
}
但是这样的话我完全可以使用 Manipulator2(HasF x)不使用泛型也可以实现,这个得好处只能是说作为一个返回值可以通过上面说的泛型的类型推断来返回一个确切的HasF类型(他自己活着其导出类)。
擦除来实现迁移兼容性
简单来说c++在编译期,会把你传入泛型有几个类型就帮你生成多少份模板,拿简单的例子,public void manipulate(T a) { a.f(); },你传入一个A类型跟一个B类型,在编译之后,会帮你生成两份模板,A.f()跟B.f(),这样就可以做到运行期确切类型信息的调用,虽然这样做你传入类型越多生成模板也多,不利于代码共享。而C#更加完善了,“首先在编译时,c#会将泛型编译成元数据,即生成.net的IL Assembly代码。并在CLR运行时,通过JIT(即时编译), 将IL代码即时编译成相应类型的特化代码。”,这样代码共享就优化了。
那既然JAVA是受到c语言的启发,那么为什么不能做得更好呢,你想想在写代码的时候,我明明知道两个类型的确切信息,虽然这他们不是同一类型,但是他们的共同的信息可以被一个泛型类调用,但是JAVA泛型擦除阻止了这个情况,导致泛型的泛化性受到了限制。原因很简单,为了“迁移兼容性”。
class SimpleHolder{
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String)holder.getObj();
class GenericHolder<T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
GenericHolder<String> holder = new GenericHolder<String>();
holder.setObj("Item");
String s = holder.getObj();
//上面一个泛型跟一个非泛型在JVM是的汇编码是一样的看下面,这样也就说,擦除可以让非泛型跟泛型代码很好的共存,不会影响到现有非泛型的汇编块!
aload_1
invokevirtual // Method get: ()Object
checkcast // class java/lang/String
astore_2
return
JAVA在SE5之后才推出泛型,那么SE5之前的类库以及大量的代码如果不通过擦除那么在JVM的汇编码将无法兼容通过泛型编写的代码,也就是说不通过擦除,SE5之前的很多类库将不能在新的版本之后运行,而类库对效率提升有很大帮助,而且推出JAVA推出泛型前已经有了很多的类库,这样做的代价太大,同时也避免了JVM的大换血。
擦除的补偿
既然擦除让我们在运行期需要确切类型信息的操作(instanceof关键字,new,转型等)都无法工作
补偿一:如果想使用instanceof可以使用T.isInstance(类型)。
补偿二:new不成功,一部分是因为擦除,一部分是编译器无法验证泛型是否有默认构造器。可以使用工厂对象Class通过类型便签Class class = T.newInstance(),但是如果你传入的类型没有默认构造器则会出问题,但是在编译的时候无法检测,等你运行时才会报错。
1.这样可以通过显式工厂对象(如果使用newInstance()这是在运行时才会执行,所以是隐式的),也可以叫做工厂设计模式
interface FactoryI<T> {
T create();
}
class Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
}
// ...
}
//通过IntegerFactory工厂返回 new Integer(0),然后传入上面的x=new Integer(0),这样如果你在下面的工厂类写错了,在写代码的时候就会报错了,编码了运行才报错的尴尬。
class IntegerFactory implements FactoryI<Integer> {
public Integer create() {
return new Integer(0);
}
}
public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory());
}
}
2.通过模板方法设计模式
//在构造自己的时候调用自己的方法创建一个自己编写的返回类型
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() { element = create(); }
abstract T create();
}
泛型数组使用ArrayList来代替,如果真的想用到数组,你要知道的因为擦除,所以T[]实际上就是Object[],要牢记这点,不然当运行起来可能会让我们折返回来重构代码。
边界
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; }
T getItem() { return item; }
}
class Colored2<T extends HasColor> extends HoldItem<T> {
Colored2(T item) { super(item); }
java.awt.Color color() { return item.getColor(); }
}
class ColoredDimension2<T extends Dimension & HasColor>
extends Colored2<T> {
ColoredDimension2(T item) { super(item); }
int getX() { return item.x; }
int getY() { return item.y; }
int getZ() { return item.z; }
}
Colored2<T extends HasColor> 这是我们之前的用法 为了让T能够调用HasColor的方法,在 Colored2<T extends HasColor> 再重用extend,Colored2<T extends HasColor> extends HoldItem<T>,就可以调用HasColor跟HoldItem的信息,下面也一样,知道就行。
通配符
先展示数组的一种特殊行为:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
Fruit[] fruit = new Apple[10],将Apple数组赋予其基类Fruit数组的引用。编译器知道Apple是Fruit的导出类所以编译通过,并且fruit[0] = new Apple(),fruit[1] = new Jonathan(),可以将Apple或它的导出类赋值给自己编译器也能编译通过, fruit[0] = new Fruit(),fruit[0] = new Orange()编译器能通过上面这个,因为编译器知道这是向Apple数组赋予Fruit数组的引用,那Fruit和它自己的导出类Orange当然属于数组存放的类型,那么当然这样写是可以的。但是到真正运行的时候,数组机制知道这个引用需要处理的Apple数组,所以只能是Apple即他的导出类可以转型,尽管Fruit是Apple的基类,数组机制不管,他只知道我处理的是Apple类型。
这个不难理解,也容易排查,但是使用泛型可以把这种运行时才会出异常挪到了编译的时候。
Fruit[] fruit = new Apple[10];
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
数组赋予可以,但是在使用泛型容器的时候就失败,因为编译器通过泛型它只知道Fruit的List不是Apple的List,尽管我们知道两个类型有继承关系,但是编译器不知道,所以禁止了这样的赋予引用,也就在编译期防止了上面的情况。如果想让他通过,你必须让编译器知道可以改成 List<? entends Fruit> flist = new ArrayList<Apple>(),但是请注意? entends Fruit只是告诉编译器一个边界,当你flist.add(new Apple()); flist.add(new Fruit());这都是编译器禁止的,因为编译器知道这个边界,但是add()传入的参数也是? entends Fruit表示任何来自Fruit的类型,但是也正是任何类型,编译器无法保证这个转型的安全性,所以禁止了。
那既然如此,下面这个例子就比较好理解了,我在其中加入了理解。
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; //Cannot upcast 这里就是跟刚开始说的一样,编译器只知道Holder<Fruit> 不是Holder<Apple>,尽管我们自己知道这两个类型是关联的,但是编译器不知道!
Holder<? extends Fruit> fruit = Apple; // OK 给定一个边界,让编译器知道是任何继承Fruit的类型,那Holder<Apple>编译器就知道属于一种类型,所以允许了这样的赋值。
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns 'Object' 好了,上面说了泛型通过? extends Fruit知道边界,但是擦除返回Object,而这本身给自己赋值当然能够通过编译和运行
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); //Cannot call set() 但是下面这两行,就跟我们刚才那段话编译器知道这个边界,但是add()传入的参数也是? entends Fruit表示任何来自Fruit的类型,但是也正是任何类型,编译器无法保证这个转型的安全性,所以禁止了,set也一样只知道参数也是? entends Fruit,所以编译器不让通过
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK 而这里传入的Object,当然可以
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
逆变(超类型通配符)
上面说到List<? entends Fruit> flist = new ArrayList<Apple>()这个通配符无法add(),这个时候可以使用“逆变”List<? super Fruit> flist = new ArrayList<Apple>(),那这样flist.add(new Apple()); flist.add(new Fruit());就是允许的。
如果使用泛型方法的协变与通配符配合,在调用的时候可以顺畅很多,这里就不做解释了(懒~~)。
public class GenericReading {
static <T> T readExact(List<T> list) {
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
// A static method adapts to each call:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// If, however, you have a class, then its type is
// established when the class is instantiated:
static class Reader<T> {
T readExact(List<T> list) { return list.get(0); }
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fruit f = fruitReader.readExact(fruit);
// Fruit a = fruitReader.readExact(apples); // Error:
// readExact(List<Fruit>) cannot be
// applied to (List<Apple>).
}
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader =
new CovariantReader<Fruit>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1(); f2(); f3();
}
} ///:~
无界通配符(<?>)
书里长篇大论,这里做个总结方便大家理解。
List跟List<?>,前者是原生List,后者是泛型List,尽管后者通过擦除使得两者都是可以这样表示List<Object>,而如果前者调用add()传入参数时,因为是原生List所以只会给你一个警告,这样转型可能会出问题,但是使用后者,编译器则会把这个警告换成一个错误禁止你这样写。
捕获异常
当调用f2()时,调用f1(),发现传入f1需要一个确切的参数,这个时候因为f2使用的是无界通配符,所以这个时候在f2的时候捕获参数类型转换传入到f1中,这就是捕获转换。
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder); /
自限定的类型
public class Father <T extends Test>
public class Children extends Father<Children>
简单来说,这样的使用自限定类型就是上面这种用法,可以限定Children初始化传入的泛型类型只能是Children自己,在这里我们只需要区分<T extends Test>是边界,而并不是类继承的意义,所以可能会有人有一个镜子对镜子的感觉,然而<T extends Test>与类extends是不一样的概念,所以不存在这个说话。通俗一点,Children类继承泛型类型Father,Father传入的参数属于Fathe类,所以当这样创建Children类传入的参数只能是自己,做到了自限定的作用!
动态类型安全(如何使用)
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs1 = new ArrayList<Dog>();
oldStyleMethod(dogs1); // Quietly accepts a Cat
List<Dog> dogs2 = Collections.checkedList(
new ArrayList<Dog>(), Dog.class);
try {
oldStyleMethod(dogs2); // Throws an exception
} catch(Exception e) {
System.out.println(e);
}
// Derived types work fine:
List<Pet> pets = Collections.checkedList(
new ArrayList<Pet>(), Pet.class);
pets.add(new Dog());
pets.add(new Cat());
}
} /* Output:
java.lang.ClassCastException: Attempt to insert class typeinfo.pets.Cat element into collection with element type class typeinfo.pets.Dog
*///:~
异常(当一种工具使用,不做详解)
混型(混合多个类)
使用混合多个类的混型作为参数类型,当做工具看吧这里。不做详解了。
潜在类型机制(需要知道)
这其实就是一种编译器的工作方式而产生的效果起的术语,又叫做"结构化类型机制"、"鸭子类型机制"。
C++是静态类型语言(类型检查发生在编译期),Python是动态类型语言(所有类型检查发生在运行时),这两种语言都支持潜在类型机制。下面举个例子(由于不懂这两种语言,所以用JAVA来表达)
public interface Performs {
void speak();
void sit();
}
class A{
void speak();
void sit();
}
class B{
void speak();
void sit();
}
Performs p = new A();
p.speak();
p.sit();
p = new B();
p.speak();
p.sit();
A a = new B();
A和B与Performs,因为方法名一样,会把你看做是一样的类型,所以潜在类型机制会帮你转换成相同的,这在c++和Python中是可以的,其实一个在编译期,一个在运行时,所以这就更加泛化了。而我们知道如果你想在泛型中去调用方法,那么你必须使用extends来规定边界,这样编译器才会允许你调用,而这样有了边界就有了限制,相比于其他的语言如c++和Python,没那么泛化了。
缺乏潜在类型机制的补偿
反射
通过反射动态获取到类型信息进行调用也是可以的,但是需要我们多编写一些代码。书中例子好理解这就不贴了。
将一个方法应用序列
也就是使用反射跟可变参数,进行一个序列Collection,来模拟潜在类型机制
使用适配器仿真潜在类型机制
当你如果写了一个泛型方法,如果你有一个类并没有继承泛型类型,但是正好跟泛型调用方法名一样,这个时候编译器不会允许你,这个时候我们需要一个适配器来返回折中,但是需要我们多写一些代码,并且需要理解这个泛型方法跟这个参数类型的意义,多做了很多事情相较于那些自带潜在类型机制的语言。
总结
泛型常常使用“狗在猫列表中”,通过这段话来证明泛型的重要性,使用容器类时可能是我们使用泛型最经常的地方,但是泛型按照我们的理解,那应该是能够让我们写出更加通用的代码,这才是泛型给我们程序员带来的最大的好处,而也正是因为使用了擦除的泛型,导致我们需要去使用适配器模式去仿真潜在类型机制,这就需要我们在写代码的时候,为这个东西去想更多的泛化细节,需要我们多付出一些努力。