我是靠谱客的博主 甜美招牌,最近开发中收集的这篇文章主要介绍四、09【Java常用类】之Java泛型Java泛型Java泛型的作用Java泛型的优点举栗子定义简单泛型举栗子通配符泛型的使用类型擦除泛型的PECS法则E T K V N ?泛型注意事项面试题,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

今天的博客主题

       Java常用类 ——》Java泛型


Java泛型

Java泛型是在 JDK1.5 提出的一个新特性。

其本质就是:参数化类型,也就是所操作的数据类型被指定为一个参数。

参数我们知道,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。

那参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

 

泛型允许对类型进行抽象。

最常见的示例是容器类型,如集合层次结构中的容器类型。Java集合中元素的类型是多种多样的。

比如有些集合中的元素是Byte类型,有些可能是String类型等等。

Java允许构建一个元素类型为 Object 的集合,其中的元素可以是任何类型。

在Java SE 1.5之前,没有泛型的情况下,通过对类型Object的引用来实现参数的任意化,任意化带来的缺点是要作显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以在预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。因此,为了解决这一问题,便在 1.5 引入泛型。

泛型或多或少都见过,可能你认为的泛型是这样的

public class Cars<T> {}

可实际上的泛型

public class UserDetailsManagerConfigurer<B extends ProviderManagerBuilder<B>,
C extends UserDetailsManagerConfigurer<B, C>> extends UserDetailsServiceConfigurer<B, C, UserDetailsManager> {}

看了之后是不是觉得泛型从入门到放弃....不会这样,不要想的很复杂,等理解了就明白了

 

Java泛型的作用

1)泛化。可以用T代表任意类型Java语言中引入泛型是一个较大的功能增强不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为泛型化的了,这带来了很多好处。

2)类型安全。泛型的一个主要目标就是提高ava程序的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果不用泛型,则必须使用强制类型转换,而强制类型转换不安全,在运行期可能发生 ClassCastException 异常,如果使用泛型,则会在编译期就能发现该错误。

3)消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

4)向后兼容。支持泛型的Java编译器(例如JDK1.5中的Javac)可以用来编译经过泛型扩充的Java程序(Generics Java程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译。

可以说泛型在 Java 中有很重要的地位,在面向对象编程及各种设计模式中应用非常广泛。

 

Java泛型的优点

1)类型安全

泛型的主要目标是提高Java程序的类型安全。

通过知道使用泛型定义的变量的类型限制,编译器可以在非常高的层次上验证类型假设。没有泛型,这些假设就只存在于系统开发人员的头脑中。

通过在变量声明中捕获这一附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误就可以在编译时被捕获了,而不是在运行时当作ClassCastException展示出来。将类型检查从运行时挪到编译时有助于Java开发人员更早、更容易地找到错误,并可提高程序的可靠性。

2)消除强制类型转换

泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。尽管减少强制类型转换可以提高使用泛型类的代码的累赞程度,但是声明泛型变量时却会带来相应的累赞程度。在简单的程序中使用一次泛型变量不会降低代码累赞程度。但是对于多次使用泛型变量的大型程序来说,则可以累积起来降低累赞程度。所以泛型消除了强制类型转换之后,会使得代码加清晰和筒洁。

3)更高的运行效率

在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。

4)潜在的性能收益

泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,Java系统开发人员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的JVM的优化带来可能。

 

举栗子

一个被举烂了的例子:

public static void main(String[] args) {
// 元素类型为 Object 的集合,元素可以是任何类型。
List arrayList = new ArrayList();
arrayList.add("abc");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
// Object 来接集合元素,没问题。
Object o = arrayList.get(i);
System.out.println(o);
// Object 太通用了,操作甚是不方便,转成 String 或者 其他类型的数据来操作
// 竟然报错了。。。
String item = (String) arrayList.get(i);
System.out.println(item);
}
}

结果会抛出异常:java.lang.ClassCastException

因为集合是里的元素是任意类型的,编译器只能保证迭代器将返回一个对象。为了确保对 String 类型变量的赋值是类型安全的,需要强制转换,在强制转为 String 导致 类转换异常。

这种问题在编译阶段就可以解决,泛型就横空出世,应运而生了。

只需要在创建集合对象的时候,指定集合存放的类型就可以了。

List<String> stringArrayList = new ArrayList<String>();
stringArrayList.add(100); // 这种在编译阶段就提示错误

在这里有一个很大的区别。编译器可以在编译时检查程序的类型正确性。当我们说 stringArrayList 是用 List<String> 类型声明的时,这就告诉了我们一些关于变量 stringArrayList 的事情,它在任何地方和任何时候使用时都保持true,编译器会保证它。相反,cast告诉我们程序员认为在代码中的某一点上是正确的。这样做提高了程序的可读性和健壮性。

可以说泛型仅在编译阶段有效。

 

定义简单泛型

java.util 包下的列表接口和迭代器接口定义的部分内容:

public interface Iterator<E> {
E next();
boolean hasNext();
}
public interface List<E> extends Collection<E> {
void add(E x);
Iterator<E> iterator();
}

这段代码应该都很熟悉,除了尖括号中的内容。这些是接口列表和迭代器的形式类型参数的声明。

类型参数可以在泛型声明中使用,几乎可以在使用普通类型的地方使用。当然也会有一些重要的限制

在示例中,我们看到了泛型类型声明列表的调用,例如List<String>。在调用中(通常称为参数化类型),所有出现的形式类型参数(例如 E)都将替换为实际的类型参数(String).

说白了 它是这样色的:

List<E> 中的E称为类型参数变量

List<String> 中的 String 称为实际类型参数

整个称为 List<E> 泛型类型

整个 List<String> 称为参数化的类型 ParameterizedType

 

举栗子

下面两行代码合法吗?

List<String> ls = new ArrayList<String>();
List<Object> lo = ls; 

第一行代码,很明显,没有问题。

第二行可能比较棘手,这可以归结一个问题:String 集合是一个 Object 集合。那可能就是第二行也是合法了喽?

继续看

lo.add(new Object());
String s = ls.get(0); 

这里我们给 ls 和 lo 取了别名。访问 ls 一个字符串列表,通过别名lo,我们可以在其中插入任意对象。结果,ls 不再仅仅是一个字符串,当试图从中得到一些东西时,发现会有一个意外的惊喜。

然后 这种情况不会发生,Java编译器就已经防止。第2行代码将导致在编译时不通过。

一般来说,如果Foo是Bar的子类型(子类或子接口),G是某种泛型类型声明,那么G<Foo>不是G<Bar>的子类型。这可能是泛型最难学的东西,因为它违背了我们根深蒂固的直觉。

 

通配符

没有引入泛型概念之前,输出集合中所有元素的写法

void printCollection(Collection c) {
Iterator i = c.iterator();
for (int j = 0; j < c.size(); j++) {
System.out.println(i.next());
}
}

在引入泛型概念之后,增强 for 循环的写法

void printCollectionGenerics(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}

问题就来了,这个新版本远不如旧版本有用。尽管旧代码可以用任何类型的集合作为参数来调用,但新代码只接受collection<Object>,正如上面的栗子演示,它不是所有类型集合的超类型!

那什么是集合的超级类型呢?

它是:Collection<?> 翻译来就是:未知集合(collection of unknown)

元素类型与任何内容匹配的集合。它被称为通配符类型 是有原因的。可以这样写:

void printCollectionWildcards(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}

这样我们就可以称之为任何类型的集合。注意,在 printCollectionWildcards() 中,我们仍然可以从c中读取元素并给它们类型Object。这样安全的,因为不管集合的实际类型是什么,它都包含对象。

但是,向其中添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时就错误

因为我们并不知道c的元素类型代表什么,所以不能向它添加对象。add() 方法接受集合元素类型E的参数。实际类型参数是什么?,它代表某种未知类型。我们要添加的任何参数都必须是此未知类型的子类型。因为我们不知道那是什么类型,所以我们不能传递任何内容。唯一的异常是null,它是每种类型的成员。

另外,提供一个 List<?>,我们可以调用 get() 并利用结果。结果类型是未知类型,但始终知道它是一个对象。因此,可以安全地将 get() 的结果赋给 Object 类型的变量,或将其作为参数传递给预期的Object类型。

 

泛型的使用

1)泛型方法

考虑编写一个方法。这个方法可以接受一个对象数组和一个集合,并将该数组中的所有对象放入该集合中。你可能会这样去写:

public static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // compile-time error
}
}

到目前为止,你大概已经知道使用 Collection<?> 不能接受未知类型的元素。

你会想到使用Collection<Object> 作为 Collection 参数,来避免这种编译时出现的错误。

要记住,不能将对象直接推送到未知类型的集合中。

处理这些问题的方法是使用泛型方法。与类型声明一样,方法声明可以是泛型的,也就是说,可以由一个或多个类型参数参数化。

public static <T> void fromArrayToCollection2(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}

可以用任何类型的集合调用此方法,这些集合的元素类型是数组元素类型的超类型。

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// T inferred to be Object
fromArrayToCollection2(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// T inferred to be String
fromArrayToCollection2(sa, cs);
// T inferred to be Object
fromArrayToCollection2(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
// T inferred to be Number
fromArrayToCollection2(ia, cn);
// T inferred to be Number
fromArrayToCollection2(fa, cn);
// T inferred to be Number
fromArrayToCollection2(na, cn);
// T inferred to be Object
fromArrayToCollection2(na, co);
// compile-time error
fromArrayToCollection2(na, cs);

定义一个泛型方法:泛型是先定义后使用,传进来是什么类型,返回就是什么类型。

public <T> void genericMethods(T t) {
System.out.println(t);
}

2)泛型类

泛型类就是把泛型定义在类上。

在使用该类的时候,才把类型明确下来。这样的话,使用的时候明确了什么类型,那么该类就代表着什么类型。在使用的时候就不用担心强转的问题,运行时转换异常的问题了。

定义一个泛型类:在类上定义的泛型,在类的方法中也可以使用。

public class ObjectTool<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}

想要那种类型就在创建的时候指定那种类型,在使用的时候,这个泛型类就会自动转成指定的类型。

// 创建对象,指定元素类型
ObjectTool<String> tool = new ObjectTool<>();
tool.setObj(new String("Hello World"));
String s = tool.getObj();
System.out.println(s);
// 创建对象,指定元素类型
ObjectTool<Integer> objectTool = new ObjectTool<>();
objectTool.setObj(10); // 如果在这个对象里传入String类型,那么编译时期就不对
int i = objectTool.getObj();
System.out.println(i);

泛型类是拥有泛型这个特性的类,但它本质上还是一个Java类,还是可以被继承的。

两种方式继承

1)子类明确泛型类的类型参数变量

2)子类不明确泛型类的类型参数变量

 

3)泛型接口

把泛型定义在接口上

public interface ObjectTool<T> {
void print(T t);
}

实现泛型接口的类(子类明确泛型类的类型参数变量)

public class StringToolImpl implements ObjectTool<String> {
@Override
public void print(String s) {
System.out.println(s);
}
}

实现泛型接口的类(子类不明确泛型类的类型参数变量)

当子类不明确泛型类的类型参数变量时,外界使用子类的时候,也需要传递类型参数变量进来,在实现类上需要定义出类型参数变量.

public class IntegerToolImpl<T> implements ObjectTool<T> {
@Override
public void print(T t) {
System.out.println(t);
}
}

需要注意的是

1)实现类要是重写父类的方法,返回值的类型需要和父类一样

2)类上声明的泛形只对非静态成员有效

 

类型擦除

 

泛型的PECS法则

PE:Producer Extends 对应 <? extends T>

CS:Consumer Super 对应 <? super T>

怎么理解这两个呢? 先看一段代码。。。

JDK8 里的一段源码。出自:Collections.copy();

/**
* Copies all of the elements from one list into another.
After the
* operation, the index of each copied element in the destination list
* will be identical to its index in the source list.
The destination
* list must be at least as long as the source list.
If it is longer, the
* remaining elements in the destination list are unaffected. <p>
*
* This method runs in linear time.
*
* @param
<T> the class of the objects in the lists
* @param
dest The destination list.
* @param
src The source list.
* @throws IndexOutOfBoundsException if the destination list is too small
*
to contain the entire source List.
* @throws UnsupportedOperationException if the destination list's
*
list-iterator does not support the <tt>set</tt> operation.
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}

<? extends T> 的上限是 T ,参数化类型只能是 T 或者 T 的子类

public class ProductExtendsSample {
static class Car {}
static class BMW extends Car {}
static class BMWX5 extends BMW {}
static class BMWZ4 extends BMW {}
public static void main(String[] args) {
List<? extends BMW> bmws = new ArrayList<>();
bmws.add(new Car());
// compile error
bmws.add(new BMW());
// compile error
bmws.add(new BMWX5()); // compile error
bmws.add(new BMWZ4()); // compile error
bmws = new ArrayList<BMW>();
// compile success
bmws = new ArrayList<BMWX5>();
// compile success
bmws = new ArrayList<Car>(); // compile error
bmws = new ArrayList<? extends BMW>(); // compile error(通配符类型无法实例化)
BMW bmw = bmws.get(0); // compile success
}
}

添加元素:

1)赋值是参数化类型为 BMW 的集合和其子类的集合都可以成功,通配符类型无法实例化。

2)编译器会阻止将 BMWZ4 类加入 bmws。在向 bmws 中添加元素时,编译器会检查类型是否符合要求。因为编译器只知道 bmws 是 BMW 某个子类的集合,但并不知道这个子类具体是什么类,为了类型安全,只好阻止向其中加入任何子类。

3)那加入 BMW 呢? NO 。实际上,不能往使用了<? extends T> 的数据结构里写入任何的值。

获取元素:

由于编译器知道它总是 BMW 的子类型,因此可以从中读取出 BMW 对象:

BMW bmw = bmws.get(0);

可以发现:<? extends T> 只能获取元素,但是不能往里写。

<? super T> 的下限是 T ,参数化类型只能是 T 或者 T 的父类

public class ConsumerSuperSample {
static class Car {}
static class BMW extends Car {}
static class BMWX5 extends BMW {}
static class BMWZ4 extends BMW {}
public static void main(String[] args) {
List<? super BMW> bmws = new ArrayList<>();
bmws.add(new Car());
// compile error
bmws.add(new BMW());
// compile success
bmws.add(new BMWX5()); // compile success
bmws.add(new BMWZ4()); // compile success
bmws = new ArrayList<BMW>();
// compile success
bmws = new ArrayList<BMWX5>();
// compile error
bmws = new ArrayList<Car>(); // compile success
bmws = new ArrayList<? extends BMW>(); // compile error(通配符类型无法实例化)
BMW bmw = bmws.get(0); // compile error
}
}

添加元素:

1) super 通配符类型同样不能实例化,BMW 和其父类的集合均可赋值

2)这里添加 BMW 及其子类均可成功。因为已经知道了 bmws 的参数化类型必定是 BMW 或其父类 T,那么 BMW 及其子类肯定可以赋值给 T。

3)出于对类型安全的考虑,我们可以加入 BMWX5 对象或者其任何子类(如BMWZ4)对象(因为编译器会自动向上转型),但由于编译器并不知道集合的内容究竟是 BMWX5 的哪个父类,因此不允许加入特定的任何父类型。

获取元素:

编译器不知道这个父类具体是什么类,只能返回Object对象,因为Object是任何 Java 类的根对象

Object bmw = bmwx5s.get(0);

可以发现:<? super T> 可以往里写,但是读的时候不行。

 

现在回头想想 Collections.copy(); 代码里的严禁性了吧。

 

E T K V N ?

E = Element (在集合中使用,因为集合中存放的是元素)
T = Type(Java 类)
K = Key(键)
V = Value(值)
N = Number(数值类型)
? = 不确定的java类型
<? extends T> = 可读不可写
<? super T> = 可写不可读

以后在Java中看到这些,不要在蒙圈了。除了这些当然还有别的,只是一个占位符,见名知意。

 

泛型注意事项

1)在定义一个泛型类时,在“<>”之间定义形式类型参数,例如:“class TestGen<K,V>”,其中“K”,“V”不代表值,而是表示类型。

2)实例化泛型对象时,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。

3)泛型中<Kextends ObjecD,extends>并不代表继承,它是类型范围限制。

4)使用泛型时,泛型类型必须为引用数据类型,不能为基本数据类型,Java中的普通方法,构造方法,静态方法中都可以使用泛型,方法使用泛型之前必须先对泛型进行声明,可以使用任意字母,一般都要大写。

5)不可以用一个基本类型(如int float)来替换泛型。

6)运行时类型检查,不同类型的泛型类是等价的(Pair与Pair是属于同一个类型Pair),这一点要特别注意,即如果obj instance of Pai == true的话,并不代表objget First()的返回值是一个String类型。

7)泛型类不可以继承Exception类,即泛型类不可以作为异常被抛出。

8)不可以定义泛型数组。

9)不可以用泛型构造对象,即:first = new T();是错误的。

10)在static方法中不可以使用泛型,泛型变量也不可以用static关键字来修饰

11)不要在泛型类中定义equals(Tx)这类方法,因为Object类中也有equals方法,当泛型类被擦除后,这两个方法会冲突。

12)根据同一个泛型类衍生出来的多个类之间没有任何关系,不可以互相赋值。

13)若某个泛型类还有同名的非泛型类,不要混合使用,坚持使用泛型类。

 

面试题

1)这段代码打印什么?

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

肯定有很多人说 false,那就真 false 了。它打印true,因为通用类的所有实例都具有相同的运行时类,而不管其实际类型参数如何。

可以这么说:泛型类型在逻辑上看以看成是多个不同的类型,但实际上都是相同的基本类型。

2)Object 跟 T E 这些标记符代表的java类型有什么区别? 

Object 是所有类的根类,任何类的对象都可以设置给该 Object 引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。

3)List<? extends T> 和 List <? super T>有什么区别?

List<? extends T>可以接受任何继承自T的类型的List

List<? super T>可以接受任何T的父类构成的List

【例】List<? extends Number> 可以接受 List<Integer> 或 List<Float>

4)泛型中占位符 T 和 ? 有什么区别?

?和 T 都表示不确定的类型。区别在于我们可以对 T 进行操作,但是对 ? 不行。

T 是一个确定的类型,通常用于泛型类和泛型方法的定义。

?是一个不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

通过 T 可以确保泛型参数的一致性,?可以进行扩展限定。

5)Class<T> 和 <Class<?> 有什么区别呢?

常见于反射场景下的使用

 

泛型是专家组花了5年时间才搞出来的一套规范。真的很抽象....


 

 

最后

以上就是甜美招牌为你收集整理的四、09【Java常用类】之Java泛型Java泛型Java泛型的作用Java泛型的优点举栗子定义简单泛型举栗子通配符泛型的使用类型擦除泛型的PECS法则E T K V N ?泛型注意事项面试题的全部内容,希望文章能够帮你解决四、09【Java常用类】之Java泛型Java泛型Java泛型的作用Java泛型的优点举栗子定义简单泛型举栗子通配符泛型的使用类型擦除泛型的PECS法则E T K V N ?泛型注意事项面试题所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(39)

评论列表共有 0 条评论

立即
投稿
返回
顶部