Java泛型学习--第一篇

时间:2023-03-10 00:25:47
Java泛型学习--第一篇

还是那句话,学习某个知识一定要想想为什么要学它,这方面的知识用来解决什么问题的,怎么用,并且要总结的体系化,不能散的到处都是,方便以后查看博客。

今天参考廖雪峰老师官网学习并总结下泛型廖老师官网

1.什么是泛型

1.1引出泛型

泛型是一种“代码模板”,可以用一套代码套用各种类型。

Java泛型学习--第一篇

在讲解什么是泛型之前,我们先观察Java标准库提供的ArrayList,它可以看作“可变长度”的数组,因为用起来比数组更方便。

实际上ArrayList内部就是一个Object[]数组,配合存储一个当前分配的长度,就可以充当“可变数组”:

public class ArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}

如果用上述ArrayList存储String类型,会有这么几个缺点:

  • 取出时需要强制转型(如(String),这样写);

  • 不方便,易出错。

例如,代码必须这么写:

ArrayList list = new ArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String first = (String) list.get(0);

很容易出现ClassCastException,因为容易“误转型”:

list.add(new Integer(123));
// ERROR: ClassCastException:
String second = (String) list.get(1);

要解决上述问题,我们可以为String单独编写一种ArrayList:

public class StringArrayList {
private String[] array;
private int size;
public void add(String e) {...}
public void remove(int index) {...}
public String get(int index) {...}
}

这样一来,存入的必须是String,取出的也一定是String,不需要强制转型,因为编译器会强制检查放入的类型:

StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));

问题暂时解决。

然而,新的问题是,如果要存储Integer,还需要为Integer单独编写一种ArrayList:

public class IntegerArrayList {
private Integer[] array;
private int size;
public void add(Integer e) {...}
public void remove(int index) {...}
public Integer get(int index) {...}
}

实际上,还需要为其他所有class单独编写一种ArrayList:

  • LongArrayList
  • DoubleArrayList
  • PersonArrayList

    ...

    这是不可能的,JDK的class就有上千个,而且它还不知道其他人编写的class。

为了解决新的问题,我们必须把ArrayList变成一种模板:ArrayList,代码如下:

public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}

T可以是任何class。这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:

// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();

因此,泛型就是定义一种模板,例如ArrayList,然后在代码中为用到的类创建对应的ArrayList<类型>:

ArrayList<String> strList = new ArrayList<String>();

由编译器针对类型作检查:

strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!

这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。

1.2泛型的向上转型

在Java标准库中的ArrayList实现了List接口,它可以向上转型为List:

public class ArrayList<T> implements List<T> {
...
} List<String> list = new ArrayList<String>();
//List引用指向了ArrayList这个子类对象,故称为向上转型

即类型ArrayList可以向上转型为List。

要特别注意:不能把ArrayList向上转型为ArrayList或List。

这是为什么呢?假设ArrayList可以向上转型为ArrayList,观察一下代码:

// 创建ArrayList<Integer>类型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一个Integer:
integerList.add(new Integer(123));
// “向上转型”为ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一个Float,因为Float也是Number:
numberList.add(new Float(12.34));
// 从ArrayList<Integer>获取索引为1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!

我们把一个ArrayList转型为ArrayList类型后,这个ArrayList就可以接受Float类型,因为Float是Number的子类。但是,ArrayList实际上和ArrayList是同一个对象,也就是ArrayList类型,它不可能接受Float类型, 所以在获取Integer的时候将产生ClassCastException。

实际上,编译器为了避免这种错误,根本就不允许把ArrayList转型为ArrayList。

ArrayList和ArrayList两者完全没有继承关系。即有没有继承关系看的不是泛型,而是类型本身

同时思考下,廖老师并没有讲的向下转型,我想只要大家懂了向上转型。向下转型自然就理解了,也是不可以的。

1.3小结

泛型就是编写模板代码来适应任意类型;

泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查,将运行期的异常提前到了编译期,并避免了强制转换的出现,同时提高了代码的复用性;

注意泛型的继承关系:可以把ArrayList向上转型为List(T不能变!),但不能把ArrayList向上转型为ArrayList(T不能变成父类)。

再来一张图嘿嘿,黑马的,总结的不错也,定义更好,形参和实参方面去理解下再:

Java泛型学习--第一篇

2.使用泛型:

使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object,即你虽然支持写泛型,但是我就是不用现在:

// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);

此时,只能把当作Object使用,没有发挥泛型的优势,代码还需要强转,并且可以添加各种类型,可能导致异常,编译器也没有检查。

当我们定义泛型类型后,List的泛型接口变为强类型List:

// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);

当我们定义泛型类型后,List的泛型接口变为强类型List:

List<Number> list = new ArrayList<Number>();
list.add(new Integer(123));
list.add(new Double(12.34));
Number first = list.get(0);
Number second = list.get(1);

编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型,jdk1.7以后就支持了。例如,对于下面的代码:

List<Number> list = new ArrayList<Number>();

编译器看到泛型类型List就可以自动推断出后面的ArrayList的泛型类型必须是ArrayList,因此,可以把代码简写为:

// 可以省略后面的Number,编译器可以自动推断泛型类型:
List<Number> list = new ArrayList<>();

2.1使用泛型接口

除了ArrayList使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable这个泛型接口:

public interface Comparable<T> {
/**
* 返回负数: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回正数: 当前实例比参数o大
*/
int compareTo(T o);
}

可以直接对String数组进行排序:

// sort
import java.util.Arrays; public class Main {
public static void main(String[] args) {
String[] ss = new String[] { "Orange", "Apple", "Pear" };
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));
}
}

运行结果:[Apple, Orange, Pear]

这是因为String本身已经实现了Comparable接口。如果换成我们自定义的Person类型试试:

// sort
import java.util.Arrays; public class Main {
public static void main(String[] args) { Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));
}
}
class Person {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return this.name + "," + this.score;
}
}

运行程序,我们会得到ClassCastException,即无法将Person转型为Comparable。我们修改代码,让Person实现Comparable接口:

异常信息:Exception in thread "main" java.lang.ClassCastException: class Person cannot be cast to class java.lang.Comparable (Person is in unnamed module of loader com.sun.tools.javac.launcher.Main$MemoryClassLoader @52e677af;

// sort
import java.util.Arrays; public class Main {
public static void main(String[] args) {
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));
}
}
class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public String toString() {
return this.name + "," + this.score;
}
}

运行结果:[Alice,88, Bob,61, Lily,75]

运行上述代码,可以正确实现按name进行排序。

也可以修改比较逻辑,例如,按score从高到低排序。请自行修改测试。

2.2小结

使用泛型时,把泛型参数替换为需要的class类型,例如:ArrayList,ArrayList等;

可以省略编译器能自动推断出的类型,例如:List list = new ArrayList<>();;

不指定泛型参数类型时,编译器会给出警告,且只能将视为Object类型;

可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

补充:如果对Java 中 Comparable 和 Comparator 还不太了解,可以看下此博客,Comparable是类实现接口后该类支持排序,像jdk中的类,很多已经实现了此接口,有默认的排序规则,而实现Comparator则是临时定义的类排序规则。点击查看

3.泛型编写

3.1编写泛型类

编写泛型类比普通类要复杂。通常来说,泛型类一般用在集合类中,例如ArrayList,我们很少需要编写泛型类。

如果我们确实需要编写一个泛型类,那么,应该如何编写它?

可以按照以下步骤来编写一个泛型类。

首先,按照某种类型,例如:String,来编写类:

public class Pair {
private String first;
private String last;
public Pair(String first, String last) {
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}

然后,标记所有的特定类型,这里是String

public class Pair {
private String first;
private String last;
public Pair(String first, String last) {
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}

最后,把特定类型String替换为T,并申明:

public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}

熟练后即可直接从T开始编写。

3.2静态方法

编写泛型类时,要特别注意,泛型类型不能用于静态方法。例如:

public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... } // 对静态方法使用<T>:
public static Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}

上述代码会导致编译错误,我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T。

有些同学在网上搜索发现,可以在static修饰符后面加一个,编译就能通过:

public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... } // 可以编译通过:
public static <T> Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}

但实际上,这个和Pair类型的已经没有任何关系了。

对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。对于上面的create()静态方法,我们应该把它改为另一种泛型类型,例如,:

public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... } // 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}

这样才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

3.3多个泛型类型

泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>:

public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}

使用的时候,需要指出两种类型:

Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

3.4小结

编写泛型时,需要定义泛型类型;

静态方法不能引用泛型类型,必须定义其他类型(例如)来实现静态泛型方法,进行区分;

泛型可以同时定义多种类型,例如Map<K, V>。