Java面试—Day5
星光不问赶路人,时间不负有心人
文章目录
1、Java的异常体系
异常简介:异常是阻止当前方法继续执行的问题,如:文件找不到、网络连接失败、非法参数等等。发现异常的理想时期是编译阶段,然而编译阶段不能找出所有的异常,余下的问题必须在运行期间。
Java中的异常分为可查异常和不可查异常。
可查异常:
即编译时异常,只编译器在编译时可以发现的错误,程序在运行时很容易出现的异常状况,这些异常可以预计,所以在编译阶段就必须手动进行捕捉处理,即要用try-catch语句捕获它,要么用throws子句声明抛出,否者编译无法通过,如:IOException、SQLException以及用户自定义Exception异常。
不可查异常:
不可查异常包括**运行时异常**和**错误**,他们都是在程序运行时出现的。异常和错误的区别:异常能被程序本身处理,错误是无法处理的。**运行时异常指的是**程序在运行时才会出现的错误,由程序员自己分析代码决定是否用try...catch进行捕获处理。如空指针异常、类转换异常、数组下标越界异常等等。**错误是指**程序无法处理的错误,表示运行应用程序中较严重的问题,如系统崩溃、虚拟机错误、动态连接失败等等。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM出现的问题。
Java异常类层次结构图:
从上图可以看出Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例。
异常处理机制:
当异常发生时,将使用new在堆上创建一个异常对象,对于这个异常对象,有两种处理方式:
- 使用throw关键字将异常对象抛出,则当前执行路径被终止,异常处理机制将在其他地方寻找catch快对异常进行处理
- 使用try…catch在当前逻辑中进行捕获处理
throws 和 throw:
throws:一个方法在声明时可以使用throws关键字声明可能会产生的若干异常
throw:抛出异常,并退出当前方法或作用域
使用 finally 清理:
在 Java 中的 finally 的存在并不是为了释放内存资源,因为 Java 有垃圾回收机制,因此需要 Java 释放的资源主要是:已经打开的文件或者网络连接等。
在 try 中无论有没有捕获异常,finally 都会被执行。
参考自:[https://www.jianshu.com/p/ffca876ce719]
2、说说你对Java反射的理解
反射:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
关于反射的基本使用可看:Java-反射的理解与使用-(原创)
反射是一种具有与类进行动态交互能力的一种机制。
反射的作用:
在 Java 和 Android 开发中,一般情况下下面几种场景会用到反射机制:
- 需要访问隐藏属性或者调用方法改变程序原来的逻辑,这个在开发中是很常见的,由于一些原因,系统并没有开放一些接口出来,这个时候利用反射是一个有效的解决办法。
- 自定义注解,注解就是在运行时利用反射机制来获取的。
- 在开发中动态加载类,比如在 Android 中的动态加载解决65k问题等等,模块化和插件化都离不开反射。
反射的工作原理
我们知道,每个 Java 文件最终都会被编译成一个 .class 文件,这些 Class 对象承载了这个类的所有信息,包括父类、接口、构造函数、方法、属性等等,这些class文件在程序运行时会被 ClassLoader 加载到虚拟机中。当一个类被加载以后,Java 虚拟机就会在内存中自动产生一个 Class 对象,而一般情况下用 new 来创建对象,实际上本质都是一样的,只是底层原理对我们开发者透明罢了。有了 class 对象的引用,就相当于有了 Method、Field、Constructor 的一切信息,在 Java 中,有了对象的引用就有了一切,剩下就靠自己发挥了。
3、说说你对Java注解的理解
注解(Annotion)是一个接口(前加 @),程序可以通过反射来获取指定程序元素的Annotion对象,然后通过Annotion对象来获取注解里面的元数据。
注解可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。从某些方面看,Annotion就像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中。
Annotation 的行为十分类似 public、final 这样的修饰符。每个 Annotation 具有一个名字和成员,每个 Annotation 的成员具有被称为 name=value 对的名字和值,name=value 装载了 Annotation 的信息,也就是说注解中可以不存在成员。
使用注解的基本规则:Annotation 不能影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一的执行。
关于注解的基本使用可见:https://www.jianshu.com/p/91393daaaf32
参考自:https://www.jianshu.com/p/8da24b7cf443
从JDK5开始,Java增加了Annotation,注解是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用Annotation,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。
Annotation提供了一种为程序元素(包、类、构造器、方法、成员变量、参数、局部变量)设置元数据的方法。Annotation不能运行,它只有成员变量,没有方法。Annotation跟public、final修饰符一样,都是程序元素的一部分,Annotation不能作为一个程序元素使用。
1. 定义Annotation
定义新的Annotation类型使用@interface关键字(在原有interface关键字前增加@符合),例如:
//定义注解
public @interface Test{
}
//使用注解
@Test
public class MyClass{
....
}
1.1 成员变量
Annotation只有成员变量,没有方法。Annotation的成员变量在定义中以“无形参的方法”形式声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。例如:
//定义注解
public @interface MyTag{
string name();
int age();
string id() default 0;
}
//使用注解
public class Test{
@MyTag(name="红薯",age=30)
public void info(){
......
}
}
一旦在Annotation里定义了成员变量后,使用该Annotation时就应该为该Annotation的成员变量指定值。
也可以在定义Annotation的成员变量时,为其指定默认值,指定成员变量默认值使用default关键字。
根据Annotation是否包含了成员变量,可以把Annotation分为以下两类:
-
标记Annotation
没有成员变量的Annotation被称为标记,这种Annotation仅用自身的存在与否来为我们提供信息,例如@override等
-
元数据Annotation
包含成员变量的Annotation。因为它可以接收更多的元数据,因此被称为元数据Annotation。
1.2 元注解
在定义Annotation时,也可以使用JDK提供的元注解来修饰Annotation定义。JDK提供了如下四个元注解(注解的注解,不是上诉的“元数据”)
- @Retention
- @Target
- @Documented
- @Inherited
1.2.1 @Retention
用于指定Annotation可以保留多长时间。
@Retention包含一个名为“value”的成员变量,该value成员变量是RetentionPolicy枚举类型。使用@Retention时,必须为其value指定值,value成员变量的值只能是如下三个:
-
RetentionPolicy.SOURCE
Annotation只保留在源代码中,编译器编译时,直接丢弃这种Annotation
-
RetentionPolicy.CLASS
编译器会把Annotation记录在class文件中,当运行Java程序时,JVM中不再保留该Annotation
-
RetentionPolicy.RUNTIME
编译器把Annotation记录在class文件中,当运行Java程序时,JVM会保留该Annotation,程序可以通过反射获取该Annotation的信息。
//name=value形式
//@Retention(value=RetentionPolicy.RUNTIME)
//直接指定
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTag{
String name() default "我兰";
}
1.2.2 @Target
@Target指定Annotation用于修饰哪些程序元素。@Target也包含一个名为“value”的成员变量,该value成员变量类型为ElementType[],同样为枚举类型,值有以下几个:
- ElementType.TYPE 能修饰类、接口或枚举类型
- ElementType.FIELD 能修饰成员变量
- ElementType.METHOD 能修饰方法
- ElementType.PARAMETER 能修饰参数
- ElementType.CONSTRUCTOR 能修饰构造器
- ElementType.LOCAL_VARIABLE 能够修饰局部变量
- ElementType.ANNOTATION_TYPE 能修饰注解
- ElementType.PACKAGE 能修饰包
//单个ElementType
@Target(ElementType.FIELD)
public @interface AnnTest {
String name() default "sunchp";
}
//多个ElementType
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface AnnTest {
String name() default "sunchp";
}
1.2.3 @Documented
如果定义注解时,使用了@Documented修饰定义,则在用javadoc命令生成API文档后,所有使用该注解修饰的程序元素,将会包含该注解的说明。
1.2.4 @Inherited
指定Annotation具有继承性。
1.3 基本Annotation
-
@Override
限定重写父类方法。对于子类中被@Override修饰的方法,如果存在对应的被重写的父类方法,则正确;如果不存在,则报错。@Override只能作用于方法,不能作用于其他程序元素。
-
@Deprecated
用于表示某个程序元素(类、方法等等)已过时,如果使用被@Deprecated修饰的类或方法等,编译器会发出警号。
-
@SuppressWarning
抑制编译器警号。指示被@SuppressWarning修饰的程序元素(以及该程序元素中的所有子元素,例如类以及该类中的方法…)取消显示指定的编译器警告,例如,常见的@SuppressWarning (value=“unchecked”)
-
@SafeVarargs
告诉编译器忽略可变长度参数可能引起的类型转换问题,该注解修饰的方法必须为 static。
2. 提取Annotation信息(反射)
当开发者使用了Annotation修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的代码来提取并处理Annotation信息,这些处理和提取Annotation的代码统称为APT(Annotation Processing Tool)。
JDK主要提供了两个类,来完成Annotation的提取:
-
java.lang.annotation.Annotation
这个接口是所有Annotation类型的父接口
-
java.lang.reflect.AnnotatedElement
该接口代表程序中被注解的程序元素
2.1 java.lang.annotation.Annotation
该接口源码:
package java.lang.annotation;
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
其中主要方法是annotationType(),用于返回该注解的java.lang.Class
2.2 java.lang.reflect.AnnotatedElement
接口源码:
package java.lang.reflect;
import java.lang.annotation.Annotation;
public interface AnnotatedElement {
//判断该程序元素上是否存在指定类型的注解,如果存在返回true,否则false
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
//返回该程序元素上存在的指定类型的注解,如果该类型的注解不存在,则返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
//返回该程序元素上存在的所有注解
Annotation[] getAnnotations();
Annotation[] getDeclaredAnnotations();
}
AnnotatedElement接口是所有程序元素(例如java.lang.Class、java.lang.reflect.Method、java.lang.reflect.Constructor等)的父接口。所以程序通过反射获取某个类的AnnotatedElement对象后,就可以调用该对象的isAnnotationPresent()、getAnnotation()等方法来访问注解信息。
为了获取注解信息,必须使用反射知识。
注:如果想要在运行时获取注解信息,在定义注解的时候,该注解必须要使用@Retention(RetentionPolicy.RUNTIME)修饰。
2.3 示例
2.3.1 标记Annotation
//定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTag {
}
//注解处理
class ProcessTool {
static void process(String clazz) {
Class targetClass = null;
try {
targetClass = Class.forName(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
assert targetClass != null;
for (Method method : targetClass.getMethods()) {
if (method.isAnnotationPresent(MyTag.class)) {
System.out.println("被MyTag注解修饰的方法名:" + method.getName());
} else {
System.out.println("没有被MyTag注解修饰的方法名:" + method.getName());
}
}
}
}
//测试类
public class TagTest {
@MyTag
public static void m1() {
}
public static void m2() {
}
public static void main(String[] args) {
ProcessTool.process("annotation.TagTest");
}
}
//输出
没有被MyTag注解修饰的方法名:main
被MyTag注解修饰的方法名:m1
没有被MyTag注解修饰的方法名:m2
没有被MyTag注解修饰的方法名:wait
没有被MyTag注解修饰的方法名:wait
没有被MyTag注解修饰的方法名:wait
没有被MyTag注解修饰的方法名:equals
没有被MyTag注解修饰的方法名:toString
没有被MyTag注解修饰的方法名:hashCode
没有被MyTag注解修饰的方法名:getClass
没有被MyTag注解修饰的方法名:notify
没有被MyTag注解修饰的方法名:notifyAll
2.3.2 元数据Annotation
//定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTag {
String name() default "Omooo";
int age() default 21;
}
//注解处理
class ProcessTool {
static void process(String clazz) {
Class targetClass = null;
try {
targetClass = Class.forName(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
assert targetClass != null;
for (Method method : targetClass.getMethods()) {
if (method.isAnnotationPresent(MyTag.class)) {
MyTag tag = method.getAnnotation(MyTag.class);
System.out.println("方法:" + method.getName() + " 的注解内容为:" + tag.name() + " " + tag.age());
}
}
}
}
//测试类
public class TagTest {
@MyTag
public static void m1() {
}
@MyTag(name = "当当猫", age = 20)
public static void m2() {
}
public static void main(String[] args) {
ProcessTool.process("annotation.TagTest");
}
}
//输出
方法:m1 的注解内容为:Omooo 21
方法:m2 的注解内容为:当当猫 20
3. 注解本质
- 注解实质上会被编译器编译为接口,并且继承java.lang.annotation.Annotation接口
- 注解的成员变量会被编译器编译成同名的抽象方法
- 根据Java的class文件规范,class文件中会在程序元素的属性位置记录注解信息。
4. 注解的意义
-
为编译器提供辅助信息
Annotation可以为编译器提供额外的信息,以便于检测错误,抑制警告等。
-
编译源代码时进行额外操作
软件工具可以通过处理Annotation信息来生成源代码,xml文件等等。
-
运行时处理
有一些Annotation甚至可以在程序运行时被检测、使用。
总之,注解是一种元数据,起到了“描述、配置”的作用。
引自:http://www.open-open.com/lib/view/open1423558996951.html
4、说说你对依赖注入的理解
依赖注入:可以通过这个服务来安全的注入组件到应用程序中,在应用程序部署的时候还可以选择从特定的接口属性进行注入。
参考自:
5、String为什么要设计成不可变的?
String 是 Java 中一个不可变类,所以他一旦被实例化就无法被修改。为什么要把String设计成不可变的呢?那就从内存、同步和数据结构层面上谈起。
字符串池
字符串池是方法区中的一部分特殊存储。当一个字符串被创建的时候,首先会去这个字符串池中查找。如果找到,直接返回对该字符串的引用。
下面的代码只会在堆中创建一个字符串:
String string1 = "abcd";
String string2 = "abcd";
如果字符串可变的话,当两个引用指向同一个字符串时,对其中一个做修改就会影响另外一个。
String str= "123";
str = "456";
执行过程如下:
执行第一行代码时,在堆上新建一个对象实例 123,str 是一个指向该实例的引用,引用包含的仅仅只是实例在堆上的内存地址而已。执行第二行代码时,仅仅只是改变了 str 这个引用的地址,指向了另外一个实例 456。给 String 赋值仅仅只是改变了它的引用而已,并不会真正的去改变它本来内存地址上的值。这样的好处也是显而易见的,最简单的当存在多个 String 的引用指向同一个内存地址时,改变其中一个引用的值并不会其他引用的值造成影响。
那如果我们这样写呢:
String str1 = "123";
String str2 = new String("123");
System.out.println(str1 == str2);
结果是 false。JVM为了字符串的复用,减少字符串对象的重复创建,特别维护了一个字符串常量池。第一种字面量形式的写法,会直接在字符串常量池中查找是否存在值 123,若存在直接返回这个值的引用,若不存在则创建一个值123的String对象并存入字符串常量池中。而使用new关键字,则会直接才堆上生成一个新的String对象,并不会理会常量池中是否有这个值。所以本质上 str1 和 str2 指向的内存地址是不一样的。
那么,使用 new 关键字生成的 String 对象可以进入字符串常量池吗?答案是肯定的,String 类提供了一个 native 方法 intern() 用来将这个对象加入字符串常量池:
String str1 = "123";
String str2 = new String("123");
str2=str2.intern();
System.out.println(str1 == str2);
结果为 true。str2 调用 intern() 方法后,首先会在字符串常量池中寻找是否存在值为 123 的对象,若存在直接返回该对象的引用,若不存在,加入 str2 并返回。上诉代码中,常量池中已经存在了值为 123 的 str1 对象,则直接返回 str1 的引用地址,使得 str1 和 str2 指向同一个内存地址。
缓存 Hashcode
Java 中经常会用到字符串的哈希码。例如,在HashMap中,字符串的不可变能保证其Hashcode永远保持一致,避免了一些不必要的麻烦。这也就意味着每次使用一个字符串的hashcode的时候不用重新计算一次,更加高效。
安全性
String被广泛的使用在其他类中充当参数,如果字符串可变,那么类似操作就会可能导致安全问题。可变的字符串也可能导致反射的安全问题,因为它的参数也是字符串。
不可变对象天生就是线程安全的
因为不可变对象不能被改变,所以他们可以自由的在多个线程之间共享,不需要做任何同步处理。
总之,String被设计成不可变的主要目的是为了安全和高效。当然,缺点就是要创建多余的对象而并非改变其值。
参考自:
https://www.jianshu.com/p/b1d62928552d
池中已经存在了值为 123 的 str1 对象,则直接返回 str1 的引用地址,使得 str1 和 str2 指向同一个内存地址。
缓存 Hashcode
Java 中经常会用到字符串的哈希码。例如,在HashMap中,字符串的不可变能保证其Hashcode永远保持一致,避免了一些不必要的麻烦。这也就意味着每次使用一个字符串的hashcode的时候不用重新计算一次,更加高效。
安全性
String被广泛的使用在其他类中充当参数,如果字符串可变,那么类似操作就会可能导致安全问题。可变的字符串也可能导致反射的安全问题,因为它的参数也是字符串。
不可变对象天生就是线程安全的
因为不可变对象不能被改变,所以他们可以自由的在多个线程之间共享,不需要做任何同步处理。
总之,String被设计成不可变的主要目的是为了安全和高效。当然,缺点就是要创建多余的对象而并非改变其值。
参考自:
https://www.jianshu.com/p/b1d62928552d
http://www.hollischuang.com/archives/1246