Java基础知识之泛型详解
2021/7/17 1:05:10
本文主要是介绍Java基础知识之泛型详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一: 什么是 java 泛型?
Java 泛型实质就是一种语法约束,泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
二:泛型的核心原理是什么?
泛型的核心原理其实就是泛型的 T 类型参数的原理,Java 编译器在编译阶段会将泛型代码转换为普通的非泛型代码,实质就是擦除类型参数 T 替换为限定类型(上限类型或者下限类型)(无限定类型替换为Object)且插入必要的强制类型转换操作,同时 java 编译器是通过先检查代码中泛型的类型然后再进行类型擦除,最后进行编译的。 编译器在编译时擦除所有类型相关信息,所以在运行时不存在任何类型相关信息,例如List在运行时仅用一个List来表示,这样做的目的是确保能和 Java 5 之前的版本开发二进制类库进行兼容,我们无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。
三:泛型的好处是什么?
泛型的好处其实就是约束,可以让我们编写的接口、类、方法等脱离具体类型限定而适用于更加广泛的类型,从而使代码达到低耦合、高复用、高可读、安全可靠的特点,避免主动向下转型带来的恶心可读性和隐晦的转换异常,尽可能的将类型问题暴露在 IDE 提示和编译阶段。
四:泛型类
一般的类只能使用具体的类型,要么是基本类型,要么是自定义类型。如果要编写可以用于多种类型的类呢?那么我们是不是可以想到用Object来定义类型,的确,用Object完全可以实现存储任何类型的对象,但是我们在使用容器的时候又想约束容器持有某种类型的对象。因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到此目的,需要使用泛型参数,用尖括号括住,放在类名后面,然后在使用这个类的时候,再用实际的类型替换此参数类型即可
public class FanXing<T> { public T t; public T getT() { return t; } public FanXing(T t) { this.t = t; } public static void main(String[] args) { FanXing<String> fx1 = new FanXing<>("a"); FanXing<Integer> fx2 = new FanXing<>(1); System.out.println(fx1.getT().getClass()); //java.lang.String System.out.println(fx2.getT().getClass()); //java.lang.Integer } }
五:泛型接口
public interface IFanXing<T1, T2> { T1 show1(T2 t); T2 show2(T1 t); }
public class FanXing implements IFanXing<String, Integer> { @Override public String show1(Integer t) { return String.valueOf(t); } @Override public Integer show2(String t) { return Integer.valueOf(t); } public static void main(String[] args) { FanXing fanXing = new FanXing(); System.out.println(fanXing.show1(1)); //1 System.out.println(fanXing.show2("1")); //1 } }
六:泛型方法
到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。
定义泛型方法只需将泛型参数列表置于返回值之前。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
如果使用泛型方法可以取代泛型类的话,尽量使用泛型方法取代泛型类,因为它可以使事情更清楚明白
- 普通泛型方法
public class FanXing { public <T> void f(T t){ System.out.println(t.getClass().getName()); } public static void main(String[] args) { FanXing fanXing = new FanXing(); fanXing.f(""); //java.lang.String fanXing.f(1); //java.lang.Integer fanXing.f(1.0); //java.lang.Double fanXing.f(1.0F); //java.lang.Float fanXing.f('a'); //java.lang.Character } }
- 静态泛型方法
静态方法无法访问泛型类的类型参数,如果方法需要使用泛型能力,就必须使其成为泛型方法
如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class FanXing<T>{ private T t; public static <T> void e1(){ //无法访问泛型类的类型参数t } //编译器报错 // public static void e(T t){ // // } /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void e(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void f(T t){ System.out.println(t.getClass().getName()); } public static void main(String[] args) { FanXing.f(""); //java.lang.String FanXing.f(1); //java.lang.Integer FanXing.f(1.0); //java.lang.Double FanXing.f(1.0F); //java.lang.Float FanXing.f('a'); //java.lang.Character } }
- 可变参数泛型方法
泛型方法与可变参数能够很好地共存
public class FanXing { public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<>(); for (T item : args) { result.add(item); } return result; } public static void main(String[] args) { System.out.println(FanXing.makeList("a","b","c","d"));//[a, b, c, d] } }
七:泛型数组
关于泛型数组,官方文档上面说是:在java中是不能创建一个确切的泛型类型的数组的。
一般的解决方案是:在任何想要创建泛型数组的地方都使用ArrayList摘自《Java编程思想》
也就是说下面我们这样创建泛型数组是不允许的:(右边由于编译后擦除数据类型,导致变为Object数据类型,此时可以插入各种数据类型,当赋值给左边的时候还会进行类型强转为String的时候会报错,详细可以看下面的例子)
public class FanXing<T> {} FanXing<String>[] fx1 = new FanXing<String>[10]; //error FanXing<String>[] fx2 = (Fanxing<String>[])new Object[10]; //error
其问题在于数组将跟踪他们的实际类型,而这个类型是在数组被创建的时候确定的。即使fx2已经被转型为Fanxing< String>[]类型,但是这个信息只存在编译期,运行期它仍然是Object数组。
然而真的没办法创建数组了吗?答案当然是有的
成功创建泛型数组的唯一方式就是创建一个被擦除类型的新数组,然后对其转型摘自《Java编程思想》
我们看下面这个创建,编译器是通过的
public class FanXing2 { public static void main(String[] args) { FanXing<String>[] fxArray = (FanXing<String>[]) new FanXing[10]; //OK fxArray[0] = new FanXing<String>(); //OK // fxArray[1] = new Object(); //error // fxArray[2] = new FanXing<Integer>(); //error } }
下面我们再引用sun官方文档的例子:
List<String>[] lsa = new List<String>[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK
八:泛型的擦除
1、什么是泛型的擦除?
泛型的擦除是指:java编译器在编译阶段将泛型的代码转变成非泛型代码的过程。
泛型类型只有在静态类型检查期间才出现,在此以后程序中的所有泛型类型都将被擦除,替换为他们的非泛型上界。如List这样的类型将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object
2、泛型擦除的原因
因为泛型不是java出现时就有的组成部分,泛型是Java SE5才出现的,那么为了兼容JavaSE5之前的非泛型类库,所以java才采用了折中的办法,即擦除。
3、泛型擦除的代价
泛型不能用于显式地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了。
下面看一个泛型擦除的例子:
public class FanXing { public static void main(String[] args) { List<String> stringList=new ArrayList<>(); List<Integer> integerList=new ArrayList<>(); Class stringClass=stringList.getClass(); Class integerClass=integerList.getClass(); System.out.print(stringClass == integerClass); //true } }
在代码的第4行和第5行,我们分别定义了一个接受String类型的List和一个接受Integer类型的List,按照我们正常的理解,泛型ArrayList虽然是相同的, 但是我们给它传了不同的类型参数,那么c1和2的类型应该是不同的。但是结果恰恰想法,运行程序发现二者的类型时相同的。然后我们来反编译这个类来看一下它的字节码:
由上图我们可以看出无论是ArrayList< String>还是ArrayList< Integer>都被擦除为List,最终都是Object去getClass,可见没有任何一点类型信息的事。
因此:在泛型代码的内部,无法获得任何有关泛型参数类型的信息。
接下来我们再看下一个例子:
public class FanXing<T> { public void test(Object arg){ if (arg instanceof T){} //error T a=new T(); //error T[] array=new T[100]; //error } }
当我们尝试去创建T的对象以及instanceof等操作时,都是不能编译通过的,因为编译器在编译的时候已经将类型T给擦除了。
任何在运行时需要知道确切类型信息的操作都将无法工作。
九:通配符
泛型通配符也是一个难点,不过只要理解了,之后写代码就会使你的代码更加健壮。
通配符可以分为上界、下界、无界通配符。
为了方便理解,我们需要写几个辅助类,Cat和Bird都继承于Animal,Magpie继承于Bird
public class Animal { private String name; public Animal(String name) { this.name = name; } public void eat() { System.out.print(getName() + ":eat"); } public String getName() { return name; } }
public class Cat extends Animal { public Cat(String name) { super(name); } public void jump(){ System.out.print(getName()+" jump"); } }
public class Bird extends Animal { public Bird(String name) { super(name); } public void fly() { System.out.print(getName() + " fly"); } }
public class Magpie extends Bird { public Magpie(String name) { super(name); } public void sing() { System.out.print(getName() + " sing"); } }
首先我们先来看一下不使用通配符的情况。
public class TestAnimal { public void traverse(List<Animal> list) { for (Animal animal : list) { animal.eat(); } } public static void main(String[] args) { TestAnimal testAnimal = new TestAnimal(); List<Animal> animalList = new ArrayList<>(); animalList.add(new Cat("cat")); animalList.add(new Bird("bird")); testAnimal.traverse(animalList); //编译通过 List<Bird> birdList = new ArrayList<>(); birdList.add(new Bird("bird")); birdList.add(new Magpie("magpie")); testAnimal.traverse(birdList); //编译未通过 } }
上面例子对于animalList 编译通过应该都能理解,因为Cat和Bird都属于Animal,所以能添加到animalList里面。对于下面编译未通过是因为List< Bird>并不是List< Animal>的子类,出入参数有误,因此无法通过编译。
下面我们尝试去修改traverse的代码,使他变得通用一些,不仅能接受List< Animal>参数还能接受List< Bird>参数。
通配符的上界
既然知道List< Bird>并不是List< Anilmal>的子类型,那就需要去寻找替他解决的办法, 使traverse方法变得更为通用(既可以接受List< Animal>类型,也可以接受List< Bird>等参数)。在java里解决办法就是使用通配符“?”,具体到traverse,就是将方法改为traverse(List<? extends Animal> list),当中“?”就是通配符,而“? extends Animal”则表示通配符“?”的上界为Animal,换句话说就是,“? extends Animal”可以代表Animal或其子类,可代表不了Animal的父类(如Object),因为通配符的上界是Animal。如下,为改进之后的traverse
public class TestAnimal { public void traverse(List<? extends Animal> list) { for (Animal animal : list) { animal.eat(); } } public static void main(String[] args) { TestAnimal testAnimal = new TestAnimal(); List<Animal> animalList = new ArrayList<>(); animalList.add(new Cat("cat")); animalList.add(new Bird("bird")); testAnimal.traverse(animalList); //编译通过 List<Bird> birdList = new ArrayList<>(); birdList.add(new Bird("bird")); birdList.add(new Magpie("magpie")); testAnimal.traverse(birdList); //编译通过 } }
经过上述分析,可以知道List< Animal>和List< Cat>都是List< ? extends Animal>的子类型,类似有List< Bird>,List< Magpie>也是List< ? extends Animal>的子类型。现总结如下,对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List< E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))
- G< ? extends Y> 是 G< ? extends X>的子类型(如List< ? extends Cat> 是 List< ? extends Animal>的子类型)。
- G< X> 是 G< ? extends X>的子类型(如List< Animal> 是 List< ? extends Animal>的子类型)
- G< ?> 与 G< ? extends Object>等同,如List< ?> 与List< ? extends Objext>等同。
下面我们再来看一段代码:
public class TestAnimal { public void testAdd(List<? extends Animal> list){ list.add(new Animal("animal")); //编译不通过 list.add(new Bird("bird")); //编译不通过 list.add(new Cat("cat")); //编译不通过 } public static void main(String[] args) { List<? extends Animal> animalList = new ArrayList<>(); animalList.add(new Cat("cat")); //编译不通过 animalList.add(new Bird("bird")); //编译不通过 } }
无法通过编译?是的,真的是无法通过编译!!!那到底是为什么呢?
在解释之前,再来重新强调一下已经知道的规则:在List< Aimal> list里只能添加Animal类对象及其子类对象(如Cat和Bird对象),在List< Bird>里只能添加Bird类和其子类对象(如Magpie),可不能添加Animal对象(不是Bird的子类),类似的在List< Cat>和List< Magpie>里只能添加Cat和Bird对象(或其子类对象,不过这没有列出)。现在再回头看一下testAdd()方法,我们知道List< Animal>、List< Cat>等都是List< ? extends Animal>的子类型。先假设传入的参数为为List< Animal>,则第一段代码的三个“add”操作都是可行的;可如果是List< Bird>呢??则只有第二个“add”可以执行;再假设传入的是List< Tiger>(Tiger是想象出来的,可认为是Cat的子类),则三个“add”操作都不能执行。
现在反过来说,给testAdd传入不同的参数,三个“add”操作都可能引发类型不兼容问题,而传入的参数是未知的,所以java为了保护其类型一致,禁止向List< ? extends Animal>添加任意对象,不过却可以添加 null,即list.add(null)是可行的。有了上面谈到的基础,再来理解第二段代码就不难了,因为List< ? extends Animal>的类型“? extends Animal”无法确定,可以是Animal,Bird或者Cat等,所以为了保护其类型的一致性,也是不能往list添加任意对象的,不过却可以添加 null。
总结如下:不能往List< ? extends Animal> 添加任意对象,除了null。但是子类对象可以作为方法参数赋值给右边
- 通配符的下届:
既然有了通配符的上界,自然有着通配符的下界。可以如此定义通配符的下界 List< ? super Bird>,其中”Bird“就是通配符的下界。注意:不能同时声明泛型通配符申明上界和下界。在谈注意细节之前,我们先看一下通配符的使用规则——对于通配符的上界,有以下几条基本规则:(假设给定的泛型类型为G,(如List< E>中的List),两个具体的泛型参数X、Y,当中Y是X的子类(如上的Animal和Cat))G< ? super X> 是 G< ? super Y>的子类型(如List< ? super Animal> 是 List< ? super Bird>的子类型)。G< X> 是 G< ? super X>的子类型(如List< Animal> 是 List< ? super Animal>的子类型)
我们再来看下面的代码
public class TestAnimal { public void testAdd(List<? super Bird> list){ list.add(new Bird("bird")); //编译通过 list.add(new Magpie("magpie")); //编译通过 } public static void main(String[] args) { List<? super Bird> animalList = new ArrayList<>(); animalList.add(new Bird("bird")); //编译通过 animalList.add(new Magpie("magpie")); //编译通过 animalList.add(new Animal("animal")); //编译不通过 } }
testAdd 方法 没错,编译通过!!!
在解疑之前,再来强调一个知识点,子类可以指向父类,即Bird也是Animal对象。现在考虑传入到testAdd()的所有可能的参数,可以是List< Bird>,List< Animal>,或者List< Objext>等等,发现这些参数的类型是Bird或其父类,那我们可以这样看,把bird、magpie看成Bird对象,也可以将bird、magpie看成Animal对象,类似的可看成Object对象,最后发现这些添加到List< ? supe Bird> list里的对象都是同一类对象,因此testAdd方法是符合逻辑,可以通过编译的。
现在再来看一下第二段代码对于,第二、三行代码的解释和上文一样,至于最后一行“list.add(new Animal(“animal”))”是无法通过编译的,为什么的??为了保护类型的一致性,因为“? super Bird”可以是Animal,也可以是Object或其他Bird的父类,因无法确定其类型,也就不能往List< ? super Bird>添加Bird的任意父类对象。可以添加比下界小的值。
- 无界通配符
知道了通配符的上界和下界,其实也等同于知道了无界通配符,不加任何修饰即可,单独一个“?”。如List< ?>,“?”可以代表任意类型,“任意”也就是未知类型。
List< Object>表示:持有任何Object类型的原生List,只准对Object类型,注意List<Object>不是List<其他类型>的父类
List< ?>表示:具有某种特定类型的非原生List,可以为有所类型。
当方法作为List< Object>作为类型参数时:
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + ""); System.out.println(); }
可以选择改为如下实现
public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + ""); System.out.println(); }
这样就可以兼容更多的输出,而不单纯是List< Object>,如下:
List<Integer> li = Arrays.asList(1, 2, 3); List<String> ls = Arrays.asList("one", "two", "three"); printList(li); printList(ls);
对于List<T>和List<?>的区别
通俗地说,"T"是定义类或方法时声明的东西,"?"是调用时传入的东西,二者是不同的概念。
当在外面使用一个带泛型T的类或方法时,T应该用一个实际的数据类型替换它,这里的数据类型可以是Integer,String,Boolean,也可以是"?"(wildcard,通配符)。
有两种情况可以传入"?":
1、使用过程中仅用到Object的方法,跟T的具体类型无关,像equals()等等,因为任何一个泛型肯定是Object的子类;
2、使用过程中不依赖于泛型。最典型的是Class<?>,因为Class类的方法大多跟泛型无关
是java泛型的两种用法:List<T>是泛型方法,List<?>是限制通配符
List<T>一般有两种用途:
1、定义一个通用的泛型方法。
伪代码:
public interface Dao{ List<T> getList(){}; } List<String> getStringList(){ return dao.getList();//dao是一个实现类实例 } List<Integer> getIntList(){ return dao.getList(); }
上面接口的getList方法如果定义成List<?> ,后面就会报错。‘
2、限制方法的参数之间或参数和返回结果之间的关系。
List<T> getList<T param1,T param2>
这样可以限制返回结果的类型以及两个参数的类型一致。
List<?>一般就是在泛型起一个限制作用。
伪代码:
public Class Fruit(){} public Class Apple extends Fruit(){} public void test(? extends Fruit){}; test(new Fruit()); test(new Apple()); test(new String()); //这个就会报错
总结:T就是在定义类或者方法时声明的东西,?是调用时传入的东西,传入各种类型都可以。
十:泛型实战
1、请尝试解释下面程序中每行代码执行情况及原因?
public class Test{ public static <T> T add(T x, T y){ return y; } public static void main(String[] args) { int t0 = Test.add(10, 20.8); int t1 = Test.add(10, 20); Number t2 = Test.add(100, 22.2); Object t3 = Test.add(121, "abc"); int t4 = Test.<Integer>add(10, 20); int t5 = Test.<Integer>add(100, 22.2); Number t6 = Test.<Number>add(121, 22.2); } }
t0 编译直接报错,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型,而 t0 类型为 int,所以类型错误。
t1 执行赋值成功,add 的两个参数都是 Integer,所以 T 为 Integer 类型。
t2 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Number,故 T 为 Number 类型。
t3 执行赋值成功,add 的两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级为 Object,故 T 为 Object 类型。
t4 执行赋值成功,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数。
t5 编译直接报错,add 指定了泛型类型为 Integer,所以只能 add 为 Integer 类型或者其子类的参数,不能为 Float。
t6 执行赋值成功,add 指定了泛型类型为 Number,所以只能 add 为 Number 类型或者其子类的参数,Integer 和 Float 均为其子类,所以可以 add 成功。
2、请尝试解释下面程序编译到运行的现象及原因?
ArrayList<String> arrayList1 = new ArrayList(); // 编译警告 arrayList1.add("1"); // 编译通过 arrayList1.add(1); // 编译错误 String str1 = arrayList1.get(0); //返回类型就是 String ArrayList arrayList2 = new ArrayList<String>(); // 编译警告 arrayList2.add("1"); // 编译通过 arrayList2.add(1); // 编译通过 Object object = arrayList2.get(0); // 返回类型就是 Object List<String> arrayList3 = new ArrayList<>(); //JDK7的新特性,会自动推断泛型 arrayList3.add("123"); //编译通过 arrayList3.add(123); //编译错误 new ArrayList<String>().add("11"); // 编译通过 new ArrayList<String>().add(22); // 编译错误 String string = new ArrayList<String>().get(0); // 返回类型就是 String ArrayList<String> arrayList4 = new ArrayList<Object>(); // 编译错误 ArrayList<Object> arrayList5 = new ArrayList<String>(); // 编译错误
arrayList1 其实可以实现与完全使用泛型参数一样的效果,arrayList2 完全没了泛型特性,arrayList3 是 JDK 7 支持的中规中矩写法,由于类型检查就是针对引用的,与引用的对象无关,所以有了这些结果。
arrayList4 与 arrayList5 是因为泛型中参数化类型是不支持继承关系的,切记(注意与 arrayList1 和 arrayList2 对比理解)。至于不支持的原因如下:
对于 arrayList4 首先我们假设 new ArrayList< Object>() 申请的内存赋值给了引用类型是ArrayList< Object>或 ArrayList 的 temp,接着我们向 temp 中添加了几个元素(支持 add 任何类型), 然后让ArrayList< String> arrayList4 = temp,这时候调用 arrayList4 的 get 返回的都是 String 类型(类型检测是根据引用来决定的),锅来了,temp 中存了各种奇怪的类型啊, arrayList4 拿回来直接用就可能出现 ClassCastException 异常啊,卧槽,Java 设计泛型的目的之一就是为了避免出现这种锅啊,所以 Java 直接不允许进行这样的引用传递,所以直接编译报错,扼杀在摇篮。
同理对于 arrayList5 来说类似 arrayList4,只不过最终 get 出来是从 String 转为 Object,不会出现 ClassCastException 异常,但是一样卧槽啊,我还得自己转,毛线,泛型出现之一也是为了解决这个锅啊,所以 Java 直接不允许进行这样的引用传递,所以直接编译报错,扼杀在摇篮。(ArrayList<Object>不是ArrayList<Object>的父类,不能赋值,两边要求类型一致才可以,否则要用通配符)
3、请尝试解释下面程序编译到运行的现象及原因?
List<String>[] ls1 = new ArrayList<String>[10]; //1 List<String>[] ls2 = new ArrayList[10]; //2 List<?>[] lsa = new List<?>[10]; //3
运行结果
1 编译错误。
2 正常运行,但是编译有警告。
3 正常运行。
因为 Java 规定数组的类型不可以是泛型类型变量,除非是采用通配符的方式。数组是允许把一个子类数组赋给一个父类数组变量的,譬如 Father 继承自 Son,则可以定义Father[] son=new Son[10];,如果 Java 允许泛型数组这会出现如下代码:
List<String>[] ls1 = new ArrayList<String>[10]; Object[] oj = ls1;
也就是我们能在数组 ls1 里存放任何类的对象且能够通过编译,因为在编译阶段 ls1 被认为是一个Object[],也就是 ls1 里面可以放一个 int、也可以放一个 String,当我们运行阶段取出里面的 int 并强制转换为 String 则会出现 ClassCastException,这明显违反了泛型引入的原则,所以 Java 不允许创建泛型数组。对于 2 可以编译通过但是会得到警告的解释其实是因为编译器确实不让我们实例化泛型数组,但是允许我们创建对这种数组的引用,所以我们可以创建非泛型数组转型给泛型引用。对于 3 通配符的方式,最后取出数据是要做显式的类型转换的,所以并不会存在上一个例子的问题。
4、请尝试解释下面程序编译到运行的现象及原因?
public class Test1<T> { public static T value; //1 public static T test1(T param){ //2 return null; } } public class Test2<T> { public static <T> T test2(T param){ //3 return null; } } class Test<T> { //4 这个类可以运行吗? public boolean equals(T value) { return true; } }
解答:
1 编译错误,2 编译错误,3 正常运行。
因为泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用,对象都没创建,如何确定这个泛型参数是何种类型,所以当然是错误的;而 3 之所以成功是因为在泛型方法中使用的 T 是自己在方法中定义的 T,而不是泛型类中的 T。
4 无法编译通过,因为擦除后方法 boolean equals(T) 变成了方法 boolean equals(Object),这与 Object.equals 方法是冲突的,除非重新命名引发错误的方法。
5、下面程序编译运行会有什么现象?
Vector<? extends Number> x1 = new Vector<Integer>(); //正确 Vector<? extends Number> x2 = new Vector<String>(); //编译错误 Vector<? super Integer> y1 = new Vector<Number>(); //正确 Vector<? super Integer> y2 = new Vector<Byte>(); //编译错误
解释:
本题主要考察泛型中的?通配符的上下边界扩展问题。
通配符对于上边界有如下限制:Vector< ? extends 类型1> x = new Vector<类型2>();中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的子类。
通配符对于下边界有如下限制:Vector< ? super 类型1> x = new Vector<类型2>();中的类型1指定一个数据类型,则类型2就只能是类型1或者是类型1的父类。
6、简单说说 Java 中List< Object>和原始类型List之间的区别?
解答:
区别一:原始类型和带泛型参数类型< Object>之间的主要区别是在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用 Object 作为类型可以告知编译器该方法可以接受任何类型的对象(比如 String 或 Integer)。
区别二:我们可以把任何带参数的类型传递给原始类型 List,但却不能把List传递给接受List的方法,因为会产生编译错误。
7、Java 中List< ?>和List< Object>之间的区别是什么?
解答:
这道题跟上一道题看起来很像,实质上却完全不同。List< ?>是一个未知类型的List,而List< Object>其实是任意类型的List。我们可以把List< String>, List< Integer>赋值给List< ?>,却不能把List < String>赋值给List< Object>。譬如:
List<?> listOfAnyType; List<Object> listOfObject = new ArrayList<Object>(); List<String> listOfString = new ArrayList<String>(); List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; //legal listOfAnyType = listOfInteger; //legal listOfObjectType = (List<Object>) listOfString; //compiler error – in-convertible types
所以通配符形式都可以用类型参数的形式来替代,通配符能做的用类型参数都能做。 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以能用通配符的就用通配符。 如果类型参数之间有依赖关系或者返回值依赖类型参数或者需要写操作则只能用类型参数。
8、List< ? extends T>和List < ? super T>之间有什么区别?
解答:
有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符,这两个List的声明都是限定通配符的例子,List
public static <T extends Comparable<? super T>> void sort(List<T> list) public static <T> void sort(List<T> list, Comparator<? super T> c) public static <T> void copy(List<? super T> dest, List<? extends T> src) public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)
9、< T extends E>和< ? extends E>有什么关系?
解答:
它们用的地方不一样,< T extends E>用于定义类型参数,声明了一个类型参数 T,可放在泛型类定义中类名后面、泛型方法返回值前面。 < ? extends E>用于实例化类型参数,用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是 E 或 E 的某个子类型。 虽然它们不一样,但两种写法经常可以达到相同的目的,譬如:
public void addAll(Bean<? extends E> c) public <T extends E> void addAll(Bean<T> c)
这篇关于Java基础知识之泛型详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南