Java面试---Day5

Java面试—Day5

image

星光不问赶路人,时间不负有心人

1、Java的异常体系

异常简介:异常是阻止当前方法继续执行的问题,如:文件找不到、网络连接失败、非法参数等等。发现异常的理想时期是编译阶段,然而编译阶段不能找出所有的异常,余下的问题必须在运行期间。

Java中的异常分为可查异常和不可查异常。

可查异常:

即编译时异常,只编译器在编译时可以发现的错误,程序在运行时很容易出现的异常状况,这些异常可以预计,所以在编译阶段就必须手动进行捕捉处理,即要用try-catch语句捕获它,要么用throws子句声明抛出,否者编译无法通过,如:IOException、SQLException以及用户自定义Exception异常。

不可查异常:

不可查异常包括**运行时异常**和**错误**,他们都是在程序运行时出现的。异常和错误的区别:异常能被程序本身处理,错误是无法处理的。**运行时异常指的是**程序在运行时才会出现的错误,由程序员自己分析代码决定是否用try...catch进行捕获处理。如空指针异常、类转换异常、数组下标越界异常等等。**错误是指**程序无法处理的错误,表示运行应用程序中较严重的问题,如系统崩溃、虚拟机错误、动态连接失败等等。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM出现的问题。

Java异常类层次结构图:

从上图可以看出Java通过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例。

异常处理机制:

当异常发生时,将使用new在堆上创建一个异常对象,对于这个异常对象,有两种处理方式:

  1. 使用throw关键字将异常对象抛出,则当前执行路径被终止,异常处理机制将在其他地方寻找catch快对异常进行处理
  2. 使用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 中,有了对象的引用就有了一切,剩下就靠自己发挥了。

参考自:Java反射以及在Android中的特殊应用

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. 注解的意义
  1. 为编译器提供辅助信息

    Annotation可以为编译器提供额外的信息,以便于检测错误,抑制警告等。

  2. 编译源代码时进行额外操作

    软件工具可以通过处理Annotation信息来生成源代码,xml文件等等。

  3. 运行时处理

    有一些Annotation甚至可以在程序运行时被检测、使用。

总之,注解是一种元数据,起到了“描述、配置”的作用。

引自:http://www.open-open.com/lib/view/open1423558996951.html

4、说说你对依赖注入的理解

依赖注入:可以通过这个服务来安全的注入组件到应用程序中,在应用程序部署的时候还可以选择从特定的接口属性进行注入。

参考自:

用Dagger2在Android中实现依赖注入

反射、注解与依赖注入总结

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liknana

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值