泛型目的
泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
通俗的讲:有了泛型之后,可以告诉编译器每个集合接受哪些对象类型。编译器自动地为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。
元组(tuple)
将一组对象直接打包存储于其中的一个单一对象。
泛型接口
Java泛型局限性:基本类型无法作为类型参数。
泛型方法
是否拥有泛型方法,与其所在的类是否是泛型没有关系。
如果使用泛型方法可以取代整个类泛型化,那么就应该只使用泛型方法,它使得事情更清楚更明白。
定义泛型方法,将泛型参数列表置于返回值之前。
public class GenericMethods {
/**
* 定义一个泛型方法
*/
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这也是类型参数推断(type argument inference)。所以我们可以像调用普通方法一样调用f()。
泛型还可以应用于内部类以及匿名内部类。
public interface Generator<T> {
T next();
}
class Customer {
private static long counter = 1;
private final long id = counter++;
private Customer() {
}
@Override
public String toString() {
return "Customer " + id;
}
/**
* 一种生成Generator对象的方法。
* @return
*/
public static Generator<Customer> generator() {
return new Generator<Customer>() {
@Override
public Customer next() {
return new Customer();
}
};
}
}
class Teller {
private static long counter = 1;
private final long id = counter++;
public Teller() {
}
@Override
public String toString() {
return "Teller " + id;
}
public static Generator<Teller> generator = new Generator<Teller>() {
@Override
public Teller next() {
return new Teller();
}
};
}
public class BankTeller {
public static void server(Teller t, Customer c) {
System.out.println(t + " serves " + c);
}
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> line = new LinkedList<Customer>();
Generators.fill(line, Customer.generator(), 15);
List<Teller> tellers = new ArrayList<Teller>();
Generators.fill(tellers, Teller.generator, 4);
for (Customer c : line) {
server(tellers.get(rand.nextInt(tellers.size())), c);
}
}
}
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
class Frob {
}
class Fnorkle {
}
class Quark<Q> {
}
class Particle<POSITION, MOMENTUM> {
}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> p = new Particle<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
// output
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
这是因为Java是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。
擦除减少了泛型的泛化性。
在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。
擦除主要的正当理由是非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用。
泛型不能用于显式地引用运行时类型的操作之中。
擦除例子:
/**
* 擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致型
*/
public class FilledListMarker<T> {
/**
* 即使编译器无法知道create()中的T的任何信息,但是它仍旧可以在编译期确保你放置到
* result中的对象具有T类型,使其适合ArrayList<T>.
*/
List<T> create(T t, int n) {
List<T> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
result.add(t);
}
return result;
}
public static void main(String[] args) {
FilledListMarker<String> stringMaker = new FilledListMarker<>();
List<String> list = stringMaker.create("hello",4);
System.out.println(list);
}
}
擦除的补偿
任何在运行时需要知道确切类型信息的操作都将无法工作。
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
// error
if(arg instanceof T){ }
// error
T var = new T();
// error
T[] array = new T[SIZE];
// error
T[] array = (T)new Object[SIZE];
}
}
引入类型标签来对擦除进行补偿
/**
* 编译器将确保类型标签可以匹配泛型参数
*/
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind){
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> cttl = new ClassTypeCapture<>(Building.class);
System.out.println(cttl.f(new Building()));
System.out.println(cttl.f(new House()));
ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
泛型数组
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型。
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
/**
* 公开底层表示的方法
*
* @return
*/
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
//Integer[] ia = gai.rep();
// 因为有了擦除,数组的运行时类型就只能是Object[]
Object[] oa = gai.rep();
}
}
使用类型标记Class传递到构造器中,从擦除中恢复,让我们创建需要的实际类型的数组。
public class GenericArrayWithTypeToke<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToke(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToke<Integer> gai = new GenericArrayWithTypeToke<Integer>(Integer.class, 10);
Integer[] ia = gai.rep();
}
}
边界
定义:即对象进入和离开方法的地点。这些正是编译器在编译期间执行类型检查并插入转型代码的地点。
interface SuperPower {
}
interface XRayVision extends SuperPower {
void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
void hearSubtleNoise();
}
interface SuperSmell extends SuperPower {
void trackBySmell();
}
class SuperHero<POWER extends SuperPower> {
POWER power;
SuperHero(POWER power) {
this.power = power;
}
POWER getPower() {
return power;
}
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER> {
SuperSleuth(POWER power) {
super(power);
}
void see() {
power.seeThroughWalls();
}
}
class CanineHero<POWER extends SuperHearing & SuperSmell> extends SuperHero<POWER> {
CanineHero(POWER power) {
super(power);
}
void hear() {
power.hearSubtleNoise();
}
void smell() {
power.trackBySmell();
}
}
class SuperHearSmell implements SuperHearing, SuperSmell {
@Override
public void hearSubtleNoise() {
}
@Override
public void trackBySmell() {
}
}
class DogBoy extends CanineHero<SuperHearSmell> {
DogBoy() {
super(new SuperHearSmell());
}
}
public class EpicBattle {
static <POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoise();
}
static <POWER extends SuperHearing & SuperSmell> void superFind(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoise();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
superFind(dogBoy);
}
}
通配符
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
// 编译允许,运行错误
try {
fruit[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
// 编译允许,运行错误
try {
fruit[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
// 编译报错,不相容的类型
// 不能把一个涉及Apple的泛型赋给一个涉及Fruit的泛型
List<Fruit> flist = new ArrayList<Apple>();
}
}
在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的:
public class GenericsAndCovariance {
public static void main(String[] args) {
List<? extends Fruit> flist = new ArrayList<Apple>();
// 通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”
flist.add(new Apple());
flist.add(new Fruit());
flist.add(new Object());
// 以上都会报编译错误
flist.add(null);
Fruit f = flist.get(0);
}
}
数组的协变性
public class Ex26 {
public static void main(String[] args) {
Number[] numbers = new Integer[3];
numbers[0] = new Integer(0);
numbers[1] = new Integer(1);
numbers[2] = new Integer(2);
// Number 是抽象的,不能实例
//numbers[0] = new Number();
// compile OK; runtime ArrayStoreException:
try {
numbers[1] = new Double(3.4);
} catch (Exception e) {
System.out.println(e);
}
for (Number n : numbers)
System.out.println(n);
// 编译错误,不相容的类型
// Integer[] ints = numbers;
// for(Integer n : numbers)
// System.out.println(n);
// 即使运行时类型是Integer
for (Number n : numbers)
System.out.println(n.getClass().getSimpleName());
}
}
协变性对List不起作用
public class Ex27 {
public static void main(String[] args) {
// compile error: incompatible types:
// List<Number> lnum = new ArrayList<Integer>();
List<? extends Number> nlist = new ArrayList<Integer>();
// compile error: can't add Integer:
// nlist.add(new Integer(0));
nlist.add(null); // can add null
Number x = nlist.get(0); // can get Number (null)
System.out.println(nlist);
}
}
协变和通配符总结
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());
static void f1(){
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
//但是,如果你有一个类,那么在实例化类时就会建立它的类型
static class Reader<T>{
T readExact(List<T> list){
return list.get(0);
}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruit);
}
// 协变
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitReader = new CovariantReader<>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
//Fruit c = new Apple();
}
public static void main(String[] args) {
f1();
f2();
f3();
}
}
捕获转换
如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数。未指定的通配符类型被捕获,并转换为确切的类型。
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder){
f1(holder);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
f1(raw);
f2(raw);
Holder rawBasic = new Holder();
rawBasic.set(new Object());
f2(rawBasic);
// Holder<?>将持有具有某种具体类型的同构集合。
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
}
泛型使用问题总结
任何基本类型都不能作为类型参数。
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。
interface Payable<T>{
}
class Employee implements Payable<Employee>{}
// 编译失败
class Hourly extends Employee implements Payable<Hourly>{}
public class Exam {
}
-------------------------------------------------------------------
// 如果从Payable的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码可一编译
interface Payable{
}
class Employee implements Payable{}
// 编译成功
class Hourly extends Employee implements Payable{}
由于擦除原因,重载方法将产生相同的类型签名,所以必须提供有明显区别的方法名
void f(List<T> v){}
// 编译错误
void f(List<W> v){}
void f2(List<W> v){}
一旦为Comparable确定了ComparablePet参数,那么其他任何实现类都不能与ComparablePet之外的任何对象比较
class Hamster extends ComparablePet implements Comparable<ComparablePet> {
}
class Gecko extends ComparablePet {
}
// error ComparablePet cannot be inherited with different arguments Cat and Cat
//class Cat extends ComparablePet implements Comparable<Cat>{
//
//}
public class ComparablePet implements Comparable<ComparablePet> {
@Override
public int compareTo(ComparablePet o) {
return 0;
}
}
Effective Java — Generic
每个泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。原生态类型就像从类型声明中删除了所有泛型信息一样。List相对应的原生态类型是List。
Set<?>:某个类型的集合。
将SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明。
数组是协变的,表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。泛型则是不可协变的。
利用数组,会在运行的时候发现错误;利用列表,则在编译的时候发现错误。
// 运行时报错
Object[] objectArray = new Long[1];
objectArray[0] = "I";
// 编译报错
List<Object> ol = new ArrayList<Long>();
数组时具体化的。因此数组会在运行时才知道检查它们的元素类型约束。泛型是通过擦除来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃它们的元素类型信息。
不可具体化的类型:是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。
producer-extends,consumer-super,所有的comparable和comparator都是消费者。
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is null");
}
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%S %x %s%n", favoriteString, favoriteInteger, favoriteClass.getSimpleName());
}
}