泛型

泛型

泛型的理解

1、泛型就是泛化的类型,本质是参数化类型,是jdk1.5引入的新特性,用在类、接口和方法上,即泛型类,泛型接口和泛型方法

2、为了解决利用继承Object来实现通用性导致的强制类型转换和可能发生的类型转换异常的问题。

3、泛型的好处是确保了编译时期的类型安全,和避免了强制类型转换的麻烦

4、缺陷是因为泛型使用了类型擦除机制,jvm运行之前会将泛型信息擦除掉,这样做是为了兼容jdk1.5之前的代码,但是也会导致通过反射可以跳过泛型的问题,因为运行期间并没有泛型的限制

5、泛型通配符?代表任意类型,泛型上限<? extends T>,泛型下限<? extends T>

6、jdk1.7 新特性泛型推断,声明变量时定义过泛型得话,在实例化对象时可以省略类型,但是还要加<>,否则还是原类型

泛型之前

在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性,此外,如果将方法参数声明为借口,将会更加灵活。

在jdk1.5之前,通用程序的设计就是利用继承实现的,即Object,因为Object为所有类型的基类

但是这样会面临两个问题:

1、当我们获取一个值的时候,必须进行强制类型转换

2、假定我们预想的是在 ArrayList 中存储 String 类型的值,但是因为 ArrayList 只是维护一个Object引用的数组,我们无法阻止将其他类型的数据加入到集合中。这样导致的后果是,当我们使用数据时,获取到的 Object 对象转换为我们需要的 String 类型会报异常,因为类型并不匹配,这些本可以避免的错误在编译的时候却不会收到任何错误提示!

这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。

泛型

Java泛型是JavaSE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

泛型通配符

? // 指任意类型

泛型上限

<? extends Object>{    // 可以接受任意类型

泛型下限

<? super String> // 只能接收String或Object类型的泛型,String类的父类只有Object类

泛型推断

jdk1.7新特性:编译器会根据变量声明时的泛型类型自动推断出实例化时的泛型类型

类型擦除机制

List<String> ll = new ArrayList<>();
List<Integer> kk = new ArrayList<>();
System.out.println(ll.getClass());//输出:class java.util.ArrayList
System.out.println(kk.getClass());//输出:class java.util.ArrayList
System.out.println(ll.getClass() == kk.getClass());//输出:true

1、泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。

2、在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。

3、类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如通过反射操作编译器不能进行的操作,因为运行期间的泛型都被擦除了

相关面试题

1、Java中的泛型是什么 ? 使用泛型的好处是什么?

泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。

泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。

2、Java的泛型是如何工作的 ? 什么是类型擦除?

泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。

编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了兼容jdk1.5之前的代码。

3、什么是泛型中的限定通配符和非限定通配符?

限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。

4、List<? extends T>和List <? super T>之间有什么区别?

这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出现的连接中可以找到更多信息。

5、如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?

// 编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:


public static void main(String[] args) {		
	System.out.println(get("Buffer"));
}

public static <T> T get(T t) {
	return t;
}

6、Java中如何使用泛型编写带有参数的类?

// 这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。

public static void main(String[] args) {		
	System.out.println(get("Buffer"));
}

public static <T> T get(T t) {
	return t;
}

7、编写一段泛型程序来实现 LRU 缓存?

// 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。

// 具体实现
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
	// 定义缓存键值对个数
	private int cacheSize;

	public LRUCache(int cacheSize) {
		// 调用父类的构造方法,初始容量为16,加载因子为0.75
		// 将排序模式改为访问排序,即每次访问都会重新排序,访问到的数据顺序会排在尾部
		super(16, 0.75f, true);
		this.cacheSize = cacheSize;
	}

	protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
		// 当当前键值对个数大于定义的缓存键值对个数时,每次添加新的键值对都会删除最老的键值对,实现稳定的缓存个数
		return super.size() > cacheSize;
	}
}

// 测试
public class Test {
	public static void main(String[] args) {
		LRUCache<String,Object> lruCache = new LRUCache<String, Object>(3);
		
		lruCache.put("key1", "value1");
		lruCache.put("key2", "value2");
		lruCache.put("key3", "value3");
		
		System.out.println(lruCache.get("key1"));
		
		lruCache.put("key4", "value4");
		
		Set<Entry<String,Object>> entrySet = lruCache.entrySet();
		
		Iterator<Entry<String, Object>> iterator = entrySet.iterator();
		
		while (iterator.hasNext()) {
			Entry<String, Object> next = iterator.next();
			
			System.out.println(next.getKey() + ":" + next.getValue());
		}
	}
}

结果:
value1
key3:value3
key1:value1
key4:value4
    
// 可以从结果发现,因为我们先访问了key1,这时key1被放到了链表的尾部,排列顺序为key2、key3、key1,然后进行put("key4") 的操作在尾部添加数据,这时候键值对的数量超过了缓存键值对的数量,会删除头部最没有被访问过的数据key2,此时链表的排列顺序为key3、key1、key4,和遍历的结果相同

8、可以把List传递给一个接受List

posted @ 2022-07-28 11:35  zx墨染  阅读(68)  评论(0)    收藏  举报