*/
public List asList(T… a) {
List list = new ArrayList<>();
for (T t : a)
list.add(t);
return list;
}
}
擦除与边界
说起泛型,不可避免的会说到擦除和边界,其实也很好理解。在说擦除之前,可以来看下这段代码,输出结果?
public class Tested {
public boolean startTest() {
Class c1 = new ArrayList().getClass();
Class c2 = new ArrayList().getClass();
return c1 == c2;
}
}
结果是true,为何?因为JAVA泛型就是使用擦除来实现的。在泛型代码内部,我们无法获得任何有关泛型参数类型的信息,任何具体的类型信息都被擦除了,我们唯一知道的就是在使用一个对象。
因此List< String >和List< Integer >在运行时事实上是相同类型,这两种形式都被擦除成它们的“原生”类型,即List。
再看如下代码Tested,由于T没有指明具体类型,所以这样写t.test() 是编译都无法通过的:
public class Tested {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public void sT() {
t.test(); // err : 无法编译
}
}
现有测试类TestBase:
public class TestBase {
public void test() { }
}
为了调用test 方法,我们必须协助泛型类,给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。这里使用extend 关键字来设定边界,来看下代码:
public class Tested {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public void sT() {
t.test(); // suc : 编译成功
}
}
边界< T extends TestBase >声明T必须具有TestBase 类型或其子类型,这样就可以调用test 方法了。
而泛型的类型参数会被擦除到它的第一个边界,也就是我们的编译器会把类型参数替换为它的擦除边界类,就像上面示例,T被擦除到了TestBase,就好像在类的声明中用TestBase 替换了T 一样。
泛型类型只有在类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换成他们的非泛型上界。
例如:LIst< T >会被擦除为List,而普通的类型变量 T 会被擦除成Object。因为擦除移除了类型信息,所以,用无界的泛型参数调用的方法只是那些可以用Object调用的方法。
而如果T 不指定类型,T 是没有办法用于显示地引用运行时类型的操作的,所以new T(),obj instance T, (T) obj 这些都是错误的。举个栗子:
public class Tested {
private T t;
public void set(T t) {
this.t = t;
new T(); // 无法编译
}
public T get() {
return t;
}
}
虽然T 会被擦除成了 Object,但我们依然能给这个Object t对象 赋值一个 具体类型的对象E,随后将 t 强转成 E类型,可以参考下面的栗子:
public void startTest() {
Tested td = new Tested<>();
td.set(“test”);
String obj = td.get();
}
也许你会疑惑,既然Tested 在类型检查后,编译期将泛型 T 都擦除成了Object,为何 String obj = td.get();
能直接获取到 String 对象,而不是Object 对象呢?其实这是编译器帮我们在Class文件中自动插入了转型的代码,与下面的代码是没有区别的,因为编译后的Class文件是一样的。
public class Tested {
private Object t;
public void set(Object t) {
this.t = t;
}
public Object get() {
return t;
}
}
public void startTest() {
Tested td = new Tested();
td.set(“test”);
String obj = (String) td.get();
}
说了这么多,那为何JAVA 要引入擦除呢?主要是一个兼容性问题,怎么保证低版本或未使用泛型的类库能正常被使用的问题。
例如A使用了泛型,但是A使用的类库B无泛型,怎么办?这就要在实际的“通信”中,将A的泛型信息擦除,来保证“通信”的类库都是一致的。
擦除补偿
擦除丢失了在泛型代码中执行某些操作的能力。如上面说的对于 T 的new,instance,转型操作都是错误的。这就非常难受了,好好的泛型类(接口、方法)如果不能对 T 进行上述的操作,那还有啥用。好在有刚刚提到的 extend 关键字,能给泛型设定一个边界。
这里先不说 extend,除了extend 这些通配符,其实我们还能使用 类型标签 来对于擦除进行补偿。来看段代码:
public class Tested {
private Class tc;
public void set(Class tc) {
this.tc = tc;
}
public boolean isInstance(Object obj) {
// 判断obj是否能够转型成T
return tc.isInstance(obj);
}
}
如上面类内的Class 就是我们添加的类型标签,当然其他类同理。在这里就可以使用 tc 的 isInstance 方法了。下面是测试类:
public class TestBase { }
测试Test1th:
public class Test1th { }
测试Test2th:
public class Test2th extends TestBase { }
类比较简单,来测试下:
public void startTest() {
Tested td = new Tested<>();
td.set(TestBase.class);
boolean td1 = td.isInstance(new Test1th()); // td1 == false
boolean td2 = td.isInstance(new Test2th()); // td2 == true
}
能够发现,td1 = false, td2 = true,这说明Tested 内的泛型 T 即使被擦除成了 Object,但编译器依然会帮我们确保类型标签可以匹配泛型参数。这是很重要的属性。
泛型数组
之所以将泛型数组放在后面,是因为泛型数组是擦除的受害者,讲泛型数组就离不开擦除。来看下代码:
public class Tested {
private T[] array;
@SuppressWarnings(“unchecked”)
public Tested(int size) {
array = (T[]) new Object[size];
}
public void set(int index, T item) {
array[index] = item;
}
public T[] get() {
return array;
}
}
当我们执行代码,会代码转型报错:
public void startTest() {
Tested td = new Tested<>(4);
td.set(0, 10);
try {
Integer[] array = td.get();
} catch (Exception e) {
// java.lang.Object[] cannot be cast to java.lang.Integer[]
}
}
代码转型报错。为何?还是上文说的擦除问题,即使我们将 Tested 参数类型设置成了Integer,并且在构造中进行了Integer[] 转型,但是这个信息呢,只存在于编译期的类型检查阶段,在之后它仍然是一个Object数组,所以我们无法强转。
泛型就是这般,底层规定的数组类型是Object[],那么它就只能是Object[],推翻不了。那么我们就没法得到想要的结果了吗?上面不是说了类型标记了嘛,我们再来看代码:
public class Tested {
private T[] array;
@SuppressWarnings(“unchecked”)
public Tested(Class type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void set(int index, T item) {
array[index] = item;
}
public T[] get() {
return array;
}
}
我们再来执行下,与我们的预期相同。
public void startTest() {
Tested td = new Tested<>(Integer.class, 4);
td.set(0, 10);
Integer[] array = td.get();
int t = array[0]; // t == 10
}
通配符
再看< ? extend MyClass> 之前,先看段简单代码,见下:
public class Tested {
List list1 = new ArrayList<>();
List list2 = new ArrayList<>();
public Tested() {
list1.add(new TestBase());
list2.add(new Test1th());
}
public T get(List t) {
return t.get(0);
}
public List getList1() {
return list1;
}
public List getList2() {
return list2;
}
}
测试基类TestBase:
public class TestBase { }
测试类Test1th:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
文末
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
、阿里、美团等公司2021年的面试题**,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-WQs2f9IU-1711741350783)]
【算法合集】
[外链图片转存中…(img-2JUOHrYx-1711741350783)]
【延伸Android必备知识点】
[外链图片转存中…(img-kaycsDA1-1711741350784)]
【Android部分高级架构视频学习资源】
**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!