“泛型”这个术语的意思是:“适用于许多许多的类型”。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型在编程语言中出现时,其最初的目的希望类或方法能够具备最广泛的表达能力。
泛型的主要目的之一就是用来指定容器需要持有什么类型的对象,而且由编译器来保证类型的正确性。
我们举一个简单的例子:
public static void main(String[] args) {
List list = new ArrayList();
int num = 1;
String str = "abc";
list.add(num);
list.add(str);
System.out.println(list);
}
可以看到,我们在list中添加了数字和字符串,这么做没有什么问题,因为List中默认可以添加Object及其子类的对象。
但是当我们只希望添加和使用String类型的对象时就会出现问题。
那么我们可以使用泛型来保证添加到List中的对象的“纯粹性”。
public static void main(String[] args) {
List list = new ArrayList();
String str = "abc";
list.add(str);
System.out.println(list);
}
添加泛型后,我们再向List中添加int类型的数据时,就无法通过编译了。这样做,我们就可以避免因为数据类型出现的错误的情况。
另一方面,在我们使用泛型后,编译器会自动帮我们进行类型的转换。特别是当你使用编译器自动生成foreach语句时,会自动确认类型。
public static void main(String[] args) {
List list = new ArrayList();
list.add(cat);
for (Cat cat2 : list2) {
System.out.println(cat.name);
}
}
我们先写一个不使用泛型的例子
class User {
public String name;
public long idCard;
public String toString() {
return "[usrename=" + name + ", idCard=" + idCard + "]";
}
}
public class Test {
public static void main(String[] args) {
User user = new User();
user.name = "root";
user.idCard = 1234567890;
System.out.println(user);
}
}
这时,我们发现,身份证号信息是有可能出现X字母的,使用long肯定是不合适的了。那么我们把idCard修改为String吧。
这样是解决目前的问题了。但是,如果需求再次变化呢?我们创建了一个idCard类呢?
这时候,我们如果使用泛型,就不会产生这样的问题了。
public class Test {
public static void main(String[] args) {
User user = new User();
user.name = "root";
IDCard idCard = new IDCard();
idCard.number = "123456789X";
idCard.address = "中国";
user.idCard = idCard;
System.out.println(user);
}
}
class User{
public T name;
public T idCard;
public String toString() {
return "[usrename=" + name + ", idCard=" + idCard + "]";
}
}
class IDCard{
public T number;
public T address;
public String toString() {
return "[number=" + number + ", address=" + address + "]";
}
}
注意:
类型变量使用大写形式。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T表示“ 任意类型”。
泛型接口一般用于生产器中。生产器不需要任何参数,也无需额外信息,就知道如何产生对象。
public class Test {
public static void main(String[] args) {
Generator generator = new AnimalGenerator();
Animal animal = generator.next();
System.out.println(animal);
}
}
class Animal {
}
class Cat extends Animal {
}
class Dog extends Animal {
}
interface Generator {
public T next();
}
class AnimalGenerator implements Generator {
private Class[] animalClass = { Cat.class, Dog.class };
@Override
public Animal next() {
Random random = new Random();
Animal animal = null;
try {
animal = (Animal) animalClass[random.nextInt(2)].newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return animal;
}
}
有的同学可能看到了,我们在实现类中使用了Animal作为返回类型,这是Cat和Dog的父类,那么我们是不是直接在接口中使用Animal作为返回类型就好了?
当我们定义了一个功能全面的接口,希望接口具有强大的复用性,那么指明返回类型显然是不明智的,因为猫和电子书显然不可能有同一个父类。
泛型方法使得该方法能够独立于类而产生变化。
一个基本的指导原则:无论何时,只要你能做到,你就应该尽可能使用泛型方法。也就是说,如果使用感性方法可以取代整个类泛型化,那么就应该只使用泛型方法。
public static void main(String[] args) {
Test test = new Test();
int num = 1;
String str = "abc";
test.f(num);
test.f(str);
}
public void f(T x) {
System.out.println(x.getClass().getSimpleName());
}
我们使用?来作为通配符,可以接受任何类型。
我们一般讲?当做Object类型来处理。但是List>和List并不能等同,List是List>的子类。
但是通配符有个问题,就是能接受的类型有限。
List> list = new ArrayList();
list.add("str");//编译器报错
上面的代码看似没什么问题,但是实际在编译器中就无法通过。
使用?就无法添加任何内容,除了null。为什么呢?
因为我们无法确定List到底是什么类型的。如果是List
上面的例子,我们都是使用无边界泛型。但实际上泛型是提供了边界的。
上边界: 上边界使用extends关键字,可以接受其指定类型或其子类。
下边界: 下边界使用supers关键字,可以接受其指定类型或其父类。
为了实现这一效果,Java重写了extends关键字。
我们先来定义几个需要用到的类
class Animal{
public String name;
public void eat() {
System.out.println(this.name + "吃东西");
}
}
class Cat extends Animal{
}
class Dog extends Animal{
}
测试上边界
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
cat.name = "小猫";
dog.name = "小狗";
List list = new ArrayList();
list.add(cat);
list.add(dog);
Test.eat(list);
List list1 = new ArrayList();
list1.add(cat);
Test.eat(list1);
List list2 = new ArrayList();
list2.add(dog);
Test.eat(list2);
}
public static void eat(List extends Animal> list) {
for (Animal animal : list) {
animal.eat();
}
}
}
我们可以看到无论是Animal还是Cat和Dog类型的List都可以通过编译。
可以试着去掉“? extends”,看看是否能通过编译。
测试下边界
public class Test {
public static void main(String[] args) {
Cat cat = new Cat();
Dog dog = new Dog();
cat.name = "小猫";
dog.name = "小狗";
List list = new ArrayList();
list.add(cat);
list.add(dog);
Test.eat(list);
}
public static void eat(List super Animal> list) {
for (Object animal : list) {
((Animal) animal).eat();
}
}
}
因为继承的关系,Cat和Dog都可以看做是Animal对象。所以,通过编译是没有问题的。但是,我们可以看到,在循环中,我们不得不使用Object来指代Animal。
在类型擦除这块我遇到了一些问题,因为我目前使用的Java是1.8的版本,在我反编译代码后出现了如下情况:
public void test(java.util.List);
Code:
0: return
编译后并没有擦除泛型?但是,我又编写了一段代码
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Zoo zoo = new Zoo(animal);
zoo.show();
}
}
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
class Zoo {
private T animal;
public Zoo(T animal) {
this.animal = animal;
}
public void show() {
animal.eat();//编译不通过
}
}
也就是说,实际上在Java仍旧不能识别真正的泛型,我们仍旧无法通过使用泛型来获取任何类型信息。
那么我们怎么去解决这个问题呢?
第一种方法: 使用边界。
我们在上一节说到过边界,我们可以通过边界来消除无法识别泛型带来的影响。
我们将Zoo类的声明做一个简单的修改。
class Zoo{
}
我们界定了泛型的范围,是Animal及其子类。
第二种方法: 直接传递类型。
我在网上看到的一种方法,在这里就不多说了,有兴趣大家可以自行尝试。