在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。
本文主要描述的是java面试中可能会出现的泛型和注解。说是可能会出现,但基本还是不会面试到这里啦。但是,如果你在实战中,泛型和注解在代码优化上能够起到很好的作用。在无侵入开发中,注解有着很重要的作用,所以理解好注解和泛型对我们实战过程还是很有帮助的。下面我们通过几个简单的例子来说明一下。
泛型的工作原理?有了解过类型擦除么?
在说到泛型的工作原理的时候,我们第一个想到的是类型擦除。所以我们先介绍一下类型擦除这个概念。
Java的泛型类型在编译器阶段实现,编译过程,泛型类型会被清除掉,生成的字节码中不包含任何的泛型信息,这个过程被称为类型擦除。
List<String> list = new ArrayList<>();
上面定义的list为List<String>,经过编译之后就成了List,所以在方法区里面并没有存入String的相关信息。当然我们可以通过以下一个简单的例子来说明一下类型擦除的效果:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("2");
//idea在做检查的时候会报错,无法通过编译
list.add(1);
//类型擦除之后,通过反射可以添加整型,这句话执行不会报错
list.getClass().getMethod("add", Object.class).invoke(list, 1);
}
上面的反射可以添加任何类型,不会报错,但是要通过get方法取出来的时候会报错,因为get会进行强制类型转换为String。
泛型的原型:
类型被擦除之后,在jvm中存入的是其限定类,如果没有限定,存入的是object类。换句话说,上面的list的类型被擦除之后,我们可以添加任何类型。
泛型的优缺点:
当我们了解了类型擦除之后,我们就可以知道java泛型的工作原理了,当我们定义一个泛型T的时候,这个T是在jvm中存入object类型。聊到这里,你可能会想到既然存入的是Object,那为什么不直接使用Object呢?
这个就是泛型的作用了,java在1.5之后的版本引入了泛型,在这之前可能List的强制类型转换的错误会比较多,为了更好的避免这个问题,我们在使用泛型的时候,编译器一定会做类型检查,这样能够大大减少类型转换错误。比如上面的例子定义了一个List<String>的时候,我们想添加integer的时候会直接报错。但是泛型的使用也不是没有弊端的,从上面的内容我们知道,泛型根本不会被存入编码区,所以一个泛型的类型T是无法new 出来的,并且我们不能使用T.class()。一个被经常使用的场景就是反序列化一个泛型T是不被允许的,这个时候,我们必须要传入T的具体类型,这样就增加了一个传参,具体如下。
public class GenericClass<T> {
public T getResult(String s){
//报错,T不是一个类型
T t = JSON.parseObject(s,T.class);
return t;
}
//通过传入具体的类型,在强制转换
public T getResult(String s,Class cls){
T t = (T)JSON.parseObject(s,cls);
return t;
}
}
擦除之后,如何获取类型?
上面的内容说明了泛型是如何被擦除的,那么是不是一个泛型被擦除之后,就不能获取到其对应的类型了呢。显然不是的,在java中,泛型的类型在编译的时候确实被擦除了,但却有另外一种形式来保存,这个形式被称为签名(Signatures)。我们可以通过签名来获取泛型的类型。
public class Test extends GenericClass<String> {
}
//获取类型
ParameterizedType genericType =(ParameterizedType)Test.class.getGenericSuperclass();
//输出字符串类型
System.out.println(genericType.getActualTypeArguments()[0]);
泛型的限定符
通过上面的内容,应该对java的泛型有一个大概的了解了吧,上面我们有提到泛型的原型,如果没有限定符的话,那么原型就是object。在java中,有两个限定符extends 和 super。那么这两个限定符的作用是什么?这个限定符的常见使用方式是List<? extends T>和List <? super T>,List<? extends T>是指T的子类都可以存入,List <? super T>是指T的父类都可以存入。
如何定义一个java的注解,并阐述其实现原理
在java语言中,注解是比较常见的,比如@override,而在实战中,注解有助于我们代码的解耦和无侵入性。Java的注解有三类,分别是:元注解,自定义注解,jdk自带的注解。
元注解是指在定义一个注解的时候,必须要使用元注解进行标注,@Target,@Retention,@Documented,@Inherited,这四个是java的元注解;
jdk自带的注解是java已经定义好的注解,比如@override。
自定义注解是开发者定义的注解,下面我们通过一个注解的定义来简单说明一下注解的几个基本元素。
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented()
@Inherited()
public @interface AnnotationTest {
String value()default "";
}
定义好一个注解,至少需要使用到元注解的两个@Target和@Retention。元注解的四个注解分别代表的意思是:
元注解 | 作用 | 取值 |
@Target | 运行地方 | 如下 |
@Retention | 保留时间 | Source代码中,class编译后的类,runtime运行(常用) |
@Document | 是否文档化 | 无 |
@Inhrited | 能否被继承 | 无 |
@Target的取值有以下:
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举(常用)
Java注解的实现原理是根据不同的存留时间分别实现的。Source是只保留在源码阶段,在编译的时候就会把这些信息丢弃掉,所以基本上我们不会使用到。Class是指编译之后会保存在类中,但不会到jvm中。Runtime是会一直到运行环境中保存。在runtime的过程中,实际上底层是通过反射的原理去实现的。而编译时的注解会通过apt技术对类进行扫描,如果是source的不会进行编译到class中,如果是class的话,会添加到环境中,如果是runtime的话,会通知jvm把这些数据也加载到jvm中。
自定义注解的参数支持返回以下类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
注解和泛型是我们在代码中经常用到的技术,掌握这些技术,对提高代码的可读性有大的提升,但是面试的时候,注解和泛型并不是经常被问到。注解的核心在于如何自定义一个注解,并且结合spring的aop一起使用,而泛型的核心在于理解类型擦除的概念,了解这个概念之后,对你了解泛型有很大的帮助。本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈