java深入源码级的面试题

注:都是在百度搜索整理的答案,如有侵权和错误,希告知更改。

一、哪些情况下的对象会被垃圾回收机制处理掉

 当对象对当前使用这个对象的应用程序变得不可触及(不可达)的时候,这个对象就可以被回收了。
java垃圾回收是有jvm自动执行的,不是人为操作的,所以当不存在对某对象的任何引用时,该对象就处于被jvm回收的状态,并不是马上予以销毁。
可达性(可达性算法(GC Roots Tracing):从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。)

从强到弱,不同级别的可达性反应对象的生命周期,定义如下:
如果一个对象可以被一些线程直接使用而不用通过其他引用对象,那么它就是强可达。一个新创建的对象对创建它的线程来讲就是强可达的。
如果一个对象没有强可达性,但是它可以通过一个软引用(soft reference.)来使用,那么它就具有软可达性。
如果一个对象既没有强可达性,也没有软可达性,但是它可以通过一个弱引用(weak reference)来使用,那么他就具有弱可达性。当弱引用指向的弱可达对象没有其他的引用,那么这个对象就会被回收。
如果一个对象既没有强可达性,也没有软可达性、弱可达性,他已经被finalized,并且有一些虚引用(phantom reference)指向它,那么它就具有虚可达性。
当一个对象不能通过以上的方式指向,那么这个对象就变得不可达,并因此适合被回收。

强可达(Strongly reachable)
被new出来的对象都是强可达的,他们的引用就是强引用。任何通过强引用所使用的对象都不会被GC回收。
软可达(Softly reachable)
只有当系统需要更多内存时,GC才会回收具有软可达性的对象。在内存不足前,GC保证一定回收软可达的对象。
关于软引用(SoftReference)何时应该被回收的算法依赖于不同的JVM发行版本。它往往是一个跟引用(reference)的使用频率和使用间隔有关的函数。
弱可达(Weakly reachable
它也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存岛下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚可达(Phantom reachable)
最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。

二、讲一下常见编码方式?

ASCII 码
学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。

ISO-8859-1
128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。

GBK
全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。

UTF-16
说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。

UTF-8
UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成。

三、utf-8编码中的中文占几个字节;int型几个字节?

一般是3个字节,int为四个

四、静态代理和动态代理的区别,什么场景使用?

代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
代理模式包含如下角色:

ISubject:抽象主题角色,是一个接口。该接口是对象和它的代理共用的接口。
RealSubject:真实主题角色,是实现抽象主题接口的类。
Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以操作真实对象。代理对象提供与真实对象相同的接口,以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。

静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:在程序运行时,运用反射机制动态创建而成。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。

一个关于静态代理的例子:

警匪片大家一定都不会陌生,一些有钱的人看那个不顺眼,就想着找黑帮的帮忙杀人,黑帮就帮他们做一些坏事。这里的老板就变成了RealSubject,黑帮就变成了(Proxy),这里的real和proxy只是针对杀人是谁指使的(即幕后黑手是那个)
首先定义一个共同的接口,使得RealSubject出现的地方Proxy都可以出现

/*
 * 抽象接口,对应类图中的Subject
 */ 
public interface Subject {
    public void SujectShow();
}  

然后定义一个RealSubject,真正的幕后黑手

public class RealSubject implements Subject {
    @Override
    public void SujectShow() {
// TODO Auto-generated method stub  
        System.out.println("杀人是我指使的,我是幕后黑手!By---" + getClass());
    }
} 

然后定义一个代理类,黑帮,拿钱办事,但不是幕后黑手

public class ProxySubject implements Subject {
    private Subject realSubject;//代理类中有 老板的引用。 

    public Subject TakeCall() //通过电话联系  
    {
        return new RealSubject();
    }

    public void Before() {
        System.out.println("我只是一个代理类,在做事情之前我先声明,接下来的事情跟我无关,我只是受人指使!By---" + getClass());
    }

    public void After() {
        System.out.println("正如事情还没有发生之前讲的一样,我只是个路人,上面做的事情跟我无关,我是受人指使的! By---" + getClass());
    }

    @Override
    public void SujectShow() {
// TODO Auto-generated method stub 
        Object o = TakeCall();//代理类接到了一个电话 
        if (checked(o)) //检查这个电话是不是老板打过来的  
        {
            Before();
            this.realSubject = (Subject) o;
            realSubject.SujectShow();
            After();
        } else {
            System.out.println("不好意思,你权限不够,我帮不了你!");
        }
    }

    boolean checked(Object o)  //权限检查,这年头不是谁都可以冒充老板的  
    {
        if (o instanceof RealSubject)
            return true;
        return false;
    }
} 

测试

public class ProxyTest {
    public static void main(String[] args) {
        ProxySubject proxy = new ProxySubject();
        proxy.SujectShow();
    }
}  

执行结果:
我只是一个代理类,在做事情之前我先声明,接下来的事情跟我无关,我只是受人指使!By---class ProxyMode.ProxySubject
杀人是我指使的,我是幕后黑手!By---class ProxyMode.RealSubject
正如事情还没有发生之前讲的一样,我只是个路人,上面做的事情跟我无关,我是受人指使的! By---class ProxyMode.ProxySubject

动态代理实现过程
具体有如下四步骤: 通过实现 InvocationHandler 接口创建自己的调用处理器;通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

Subjec:

/*
 * 抽象接口,对应类图中的Subject
 */
public interface Subject {
    public void SujectShow();
}  

RealSubject :

public class RealSubject implements Subject {
    @Override
    public void SujectShow() {
// TODO Auto-generated method stub  
        System.out.println("杀人是我指使的,我是幕后黑手!By---" + getClass());
    }
}  

建立InvocationHandler用来响应代理的任何调用

public class ProxyHandler implements InvocationHandler {
    private Object proxied;

    public ProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("准备工作之前:");
//转调具体目标对象的方法  
        Object object = method.invoke(proxied, args);
        System.out.println("工作已经做完了!");
        return object;
    }
}  

动态代理类测试,这个代理类中再也不用实现Subject接口,可以动态的获得RealSubject接口中的方法

public class DynamicProxy {
    public static void main(String args[]) {
        RealSubject real = new RealSubject();
        Subject proxySubject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
                new Class[]{Subject.class},
                new ProxyHandler(real));
        proxySubject.SujectShow();
        ;
    }
} 

测试结果
准备工作之前:
杀人是我指使的,我是幕后黑手!By---class ProxyMode.RealSubject
工作已经做完了!

五、Java的异常体系

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。Java异常体系结构呈树状,其层次结构图如图


java深入源码级的面试题_第1张图片
图片.png

Thorwable类所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

|——Throwable 实现类描述java的错误和异常 一般交由硬件处理
 |——Error(错误)一般不通过代码去处理,一般由硬件保护
 |——Exception(异常)
      |——RuntimeException(运行时异常)
      |——非运行时异常

 多个try-catch语句联用时的顺序
  1、顺序执行,从上到下,有一个catch子句匹配之后,后面的自动不在执行
  2、如果多个cach内的异常有父子类的关系
    一定要,子类异常在上,父类异常在下
自定义异常类型
  一般都是提供两个构造参数,一个无参一个有参数,有参数的一般是调用父类的有参构造函数,调用形式super(message)
运行时异常
RuntimeException
|——ClassCastException多态中可以使用instanceof 进行规避
|——ArithmeticException进行if判断,吐过除数为0进行return
|——NullPointerException进行if判断是否为null
|——ArrayIndexOutBondsExcetion使用数组length属性以避免数组越界。
在后面我们异常处理的时候,经常把捕获的一场装华为运行时异常抛出,尤其是写一些函数框架时。throw new RuntimeException(e);

非运行时异(受检异常) 这些异常必须做出try-catch不然编译器无法通过注意事项
1、子类覆盖父类的方法时,父类方法抛出异常,子类的覆盖方法可以不抛出异常或者抛出父类方法相同的异常,或者抛出父类方法异常的子类。
2、父类方法抛出了多个异常,子类覆盖方法时,只能抛出父类异常的子集
3、父类没有抛出异常,子类不能抛出异常。子类发生非运行时异常时,需要进行try-catch处理
4、子类不能比父类抛出更多的异常。

凡事应当向父类看齐,父类已有就应当向分类看齐。

finally块一般用于释放资源 无论程序正常与否都执行finally块
1.只有一种情况,jvm退出了System.exit(0)这时候不会执行finally的内容
2、return语句也无法阻止finally的执行
throw关键字是用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则还应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息

六、谈谈你对解析与分派的认识。

例如,对于People p = new Man();这句代码,我们把People叫做静态类型,Man叫做动态类型。静态类型在编译期可知,实际类型在运行期才可以确定下来。

解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且运行期不可变。
java虚拟机中提供了5条方法调用字节码指令,分别如下:

  • invokestatic:调用静态方法
  • invokespecial:调用实力构造器方法,私有方法和父类方法
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的四条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的静态方法,私有方法,实例构造器,父类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)。final虽然使用invokevirtual调用,但是仍然是非虚方法。非虚方法是不可以被重写的。

解析调用一定是个静态的过程,在编译期间就完全确定。而分派调用可能是静态可能是动态的。

分派

1、静态分派

静态分派的典型应用是重载,重载根据参数的静态类型而不是实际类型作为判定依据。而静态类型在编译期可知,所以静态分派发生在编译阶段。

另外虽然编译器能确定方法的重载版本,但是在很多情况下重载版本不唯一,往往只能确定一个更加合适的版本。主要原因是字面量作为参数传入是没有显式的静态类型的。只能选择一个最贴近该字面型类型的方法。实际工作中要避免出现。

关于分派与解析的关系:并不是排他关系,而是在不同层次上去筛选,确定目标方法的过程。前面说过,静态方法会在类加载时就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本也是通过静态分派来完成的。

2、动态分派

动态分派的典型应用是重写,运行期根据方法接收者的实际类型来选择方法。invokevirtual指令的工作过程是,优先寻找当前类中是否有该方法,如有直接选择该方法,若没有找到,则在父类中寻找,直到找到为止。

七、修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?

修改后的equal:
陷阱1:定义错误equals方法签名(signature)
考虑为下面这个简单类Point增加一个等价性方法:

public class Point {  
    private final int x; 
    private final int y;
  
    public Point(int x, int y) {
  this.x = x;
  this.y = y;
  }  
    public int getX() {
    return x;
  }
    public int getY() {
    return y;
  }
}

看上去非常明显,但是按照这种方式来定义equals就是错误的。

// An utterly wrong definition of equals
public boolean equals(Point other){
        return(this.getX()==other.getX()&&this.getY()==other.getY());
}

这个方法有什么问题呢?初看起来,它工作的非常完美:

Point p1=new Point(1,2);
Point p2=new Point(1,2);
Point q=new Point(2,3);
System.out.println(p1.equals(p2)); // prints true
System.out.println(p1.equals(q)); // prints false

然而,当我们一旦把这个Point类的实例放入到一个容器中问题就出现了:

HashSet coll = new HashSet();
coll.add(p1);
System.out.println(coll.contains(p2)); // prints false

为什么coll中没有包含p2呢?甚至是p1也被加到集合里面,p1和p2是是等价的对象吗?在下面的程序中,我们可以找到其中的一些原因,定义p2a是一个指向p2的对象,但是p2a的类型是Object而非Point类型:
Object p2a = p2;现在我们重复第一个比较,但是不再使用p2而是p2a,我们将会得到如下的结果:
  System.out.println(p1.equals(p2a)); // prints false
到底是那里出了了问题?事实上,之前所给出的equals版本并没有覆盖Object类的equals方法,因为他的类型不同。下面是Object的equals方法的定义

public boolean equals(Object other)

因为Point类中的equals方法使用的是以Point类而非Object类做为参数,因此它并没有覆盖Object中的equals方法。而是一种变化了的重载。在Java中重载被解析为静态的参数类型而非运行期的类型,因此当静态参数类型是Point,Point的equals方法就被调用。然而当静态参数类型是Object时,Object类的equals就被调用。因为这个方法并没有被覆盖,因此它仍然是实现成比较对象标示。这就是为什么虽然p1和p2a具有同样的x,y值,p1.equals(p2a)仍然返回了false。这也是会什么HashSet的contains方法返回false的原因,因为这个方法操作的是泛型,他调用的是一般化的Object上equals方法而非Point类上变化了的重载方法equals

八、Java中实现多态的机制是什么?

靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
多态可分为:
1.编译多态:主要是体现在重载,系统在编译时就能确定调用重载函数的哪个版本。
2.运行多态:主要体现在OO设计的继承性上,子类的对象也是父类的对象,即上溯造型,所以子类对象可以作为父类对象使用,父类的对象变量可以指向子类对象。因此通过一个父类发出的方法调用可能执行的是方法在父类中的实现,也可能是某个子类中的实现,它是由运行时刻具体的对象类型决定的。
方法的重写Overriding和重载Overloading是Java多态性的不同表现.
重写是父类与子类之间多态性的一种表现,重载是一类中多态性的表现

九、如何将一个Java对象序列化到文件里?

写入序列化数据到文件中,主要是两个对象,一个对象是FileOutputStream 对象,一个是ObjectOutputStream 对象,ObjectOutputStream 负责向指定的流中写入序列化的对象。当从文件中读取序列化数据时,主要需要两个对象,一个是FileInputStream ,一个是ObjectInputStream 对象,ObjectInputStream 负责从指定流中读取序列化数据并还原成序列化前得对象。另外,序列化的读取数据与写入的顺序相同,比如我们序列化时先写入数据A ,再写入B ,最后写入C ;那么我们再读取数据的时候,读取到的第一个数据为A ,读取到的第二个数据为B ,最后读取到的数据为C ,即:先写入先读取的原则。
在序列化一个对象的时候,这个对象必须实现java.io.Serializable 接口, Serializable 接口中不含任何方法,这个可以理解为声明该对象是可以序列化的方法吧。当我们在序列化一个对象时,有些属性我们不想序列化(可以减少数据量),那么我们可以声明该属性为瞬间态(用transient 关键字声明)。另外,静态字段也是不会被序列化的。
当我们在序列化一个对象时,如果该对象没有覆写writeObject 或者readObject 方法,那么系统将采用默认的方法进行序列化,如果序列化对象中有这两个方法,则采用对象中的这两个方法进行序列化。至于如何找到的这两个方法,通过代码我们可以跟踪到是根据反射的方式进行判断对象是否覆写这两个方法。

import java.io.*;

public class Cat implements Serializable {
    private String name;

    public Cat() {
        this.name = "new cat";
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        try {
            //FileOutputStream获得一个文件流,ObjectOutputStream链接对象和文件流,
            // 通过writeObject方法把对象写到硬盘上面,从内存中的对象写到硬盘,故是OUT流
            FileOutputStream fos = new FileOutputStream("catDemo.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            System.out.println(" 1> " + cat.getName());
            cat.setName("My Cat");
            oos.writeObject(cat);
            oos.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        try {
            //把硬盘文件中的对象写到内存,故是IN流
            FileInputStream fis = new FileInputStream("catDemo.out");
            ObjectInputStream ois = new ObjectInputStream(fis);
            cat = (Cat) ois.readObject();
            System.out.println(" 2> " + cat.getName());
            ois.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
//writeObject和readObject本身就是线程安全的,传输过程中是不允许被并发访问的。所以对象能一个一个接连不断的传过来
    }
}

十、说说你对Java反射的理解?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射就是把java类中的各种成分映射成一个个的Java对象
如图是类的正常加载过程:反射的原理在与class对象。
熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象

java深入源码级的面试题_第2张图片
图片.png

1、获取Class对象的三种方式

1.1 Object ——> getClass()

1.2 任何数据类型(包括基本数据类型)都有一个“静态”的class属性

1.3 通过Class类的静态方法:forName(String className)(常用)

public class Fanshe {

    public static void main(String[] args) {

//第一种方式获取Class对象    

        Student stu1 = new Student();//这一new 产生一个Student对象,一个Class对象。  

        Class stuClass = stu1.getClass();//获取Class对象  

        System.out.println(stuClass.getName());

//第二种方式获取Class对象  

        Class stuClass2 = Student.class;

        System.out.println(stuClass == stuClass2);//判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 

//第三种方式获取Class对象  

        try {

            Class stuClass3 = Class.forName("fanshe.Student");//注意此字符串必须是真实路径,就是带包名的类路径,包名.类名  

            System.out.println(stuClass3 == stuClass2);//判断三种方式是否获取的是同一个Class对象  

        } catch (ClassNotFoundException e) {

            e.printStackTrace();

        }

    }

}

2、获取所有构造方法:getDeclaredConstructors();

3、获取成员变量并调用:

        *1.批量的

        *1).Field[]getFields():获取所有的"公有字段"

        *2).Field[]getDeclaredFields():获取所有字段,包括:私有、受保护、默认、公有;

        *2.获取单个的:

        *1).public Field getField(String fieldName):获取某个"公有的"字段;

        *2).public Field getDeclaredField(String fieldName):获取某个字段(可以是私有的)

        *设置字段的值:

        *Field-->public void set(Object obj,Object value):

        *参数说明:

        *1.obj:要设置的字段所在的对象;

        *2.value:要为字段设置的值;

4、获取成员方法并调用:

        *1.批量的:

        *public Method[]getMethods():获取所有"公有方法";(包含了父类的方法也包含Object类)

        *public Method[]getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的)

        *2.获取单个的:

        *public Method getMethod(String name,Class...parameterTypes):

        *参数:

        *name:方法名;

        *Class...:形参的Class类型对象

        *public Method getDeclaredMethod(String name,Class...parameterTypes)

        *

        *调用方法:

        *Method-->public Object invoke(Object obj,Object...args):

        *参数说明:

        *obj:要调用方法的对象;

        *args:调用方式时所传递的实参;

十一、说说你对Java注解的理解?

注解:对程序代码本身的描述-代码元数据,一种约定的规范,包括格式、意义、作用域等。
1、Annotation的工作原理:

JDK5.0中提供了注解的功能,允许开发者定义和使用自己的注解类型。该功能由一个定义注解类型的语法和描述一个注解声明的语法,读取注解的API,一个使用注解修饰的class文件和一个注解处理工具组成。

Annotation并不直接影响代码的语义,但是他可以被看做是程序的工具或者类库。它会反过来对正在运行的程序语义有所影响。

Annotation可以冲源文件、class文件或者在运行时通过反射机制多种方式被读取。
2、@Override注解:

package java.lang;

import java.lang.annotation.*;

/**
 * Indicates that a method declaration is intended to override a
 * method declaration in a supertype. If a method is annotated with
 * this annotation type compilers are required to generate an error
 * message unless at least one of the following conditions hold:
 *
 * 
  • * The method does override or implement a method declared in a * supertype. *
  • * The method has a signature that is override-equivalent to that of * any public method declared in {@linkplain Object}. *
* * @author Peter von der Ahé * @author Joshua Bloch * @jls 9.6.1.4 @Override * @since 1.5 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }

表示一个方法声明打算重写超类中的另一个方法声明。如果方法利用此注释类型进行注解但没有重写超类方法,则编译器会生成一条错误消息。

@Override注解表示子类要重写父类的对应方法。

Override是一个Marker annotation,用于标识的Annotation,Annotation名称本身表示了要给工具程序的信息。

下面是一个使用@Override注解的例子:

class A {
    private String id;
    A(String id){
        this.id = id;
    }
    @Override
    public String toString() {
        return id;
    }
}

3、@Deprecated注解:

package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

用 @Deprecated 注释的程序元素,不鼓励程序员使用这样的元素,通常是因为它很危险或存在更好的选择。在使用不被赞成的程序元素或在不被赞成的代码中执行重写时,编译器会发出警告。

@Deprecated注解表示方法是不被建议使用的。

Deprecated是一个Marker annotation。

下面是一个使用@Deprecated注解的例子:

class A {
    private String id;

    A(String id) {
        this.id = id;
    }

    @Deprecated
    public void execute() {
        System.out.println(id);
    }

    public static void main(String[] args) {
        A a = new A("a123");
        a.execute();
    }
}

4、@SuppressWarnings注解:

package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {

    String[] value();
}

指示应该在注释元素(以及包含在该注释元素中的所有程序元素)中取消显示指定的编译器警告。注意,在给定元素中取消显示的警告集是所有包含元素中取消显示的警告的超集。例如,如果注释一个类来取消显示某个警告,同时注释一个方法来取消显示另一个警告,那么将在此方法中同时取消显示这两个警告。

根据风格不同,程序员应该始终在最里层的嵌套元素上使用此注释,在那里使用才有效。如果要在特定的方法中取消显示某个警告,则应该注释该方法而不是注释它的类。

@SuppressWarnings注解表示抑制警告。

下面是一个使用@SuppressWarnings注解的例子:

@SuppressWarnings("unchecked")
public static void main(String[] args) {
    List list = new ArrayList();
    list.add("abc");
}

5、自定义注解:

使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。

自定义最简单的注解:

public @interface MyAnnotation {

}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation
    public void execute(){
        System.out.println("method");
    }
}

5.1、添加变量:

public @interface MyAnnotation {

    String value1();
}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation(value1="abc")
    public void execute(){
        System.out.println("method");
    }
}

当注解中使用的属性名为value时,对其赋值时可以不指定属性的名称而直接写上属性值接口;除了value意外的变量名都需要使用name=value的方式赋值。
5.2、添加默认值:

public @interface MyAnnotation {

    String value1() default "abc";
}

5.3、多变量使用枚举:

public @interface MyAnnotation {

    String value1() default "abc";
    MyEnum value2() default MyEnum.Sunny;
}
enum MyEnum{
    Sunny,Rainy
}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation(value1="a", value2=MyEnum.Sunny)
    public void execute(){
        System.out.println("method");
    }
}

5.4、数组变量:

public @interface MyAnnotation {

    String[] value1() default "abc";
}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation(value1={"a","b"})
    public void execute(){
        System.out.println("method");
    }
}

6、设置注解的作用范围:

@Documented
@Retention(value=RUNTIME)
@Target(value=ANNOTATION_TYPE)
public @interface Retention

指示注释类型的注释要保留多久。如果注释类型声明中不存在 Retention 注释,则保留策略默认为 RetentionPolicy.CLASS。

只有元注释类型直接用于注释时,Target 元注释才有效。如果元注释类型用作另一种注释类型的成员,则无效。

public enum RetentionPolicy
extends Enum

注释保留策略。此枚举类型的常量描述保留注释的不同策略。它们与 Retention 元注释类型一起使用,以指定保留多长的注释。

@Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,
@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到

@Retention注解可以在定义注解时为编译程序提供注解的保留策略。

属于CLASS保留策略的注解有@SuppressWarnings,该注解信息不会存储于.class文件。

6.1、在自定义注解中的使用例子:

@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {

    String[] value1() default "abc";
}

7、使用反射读取RUNTIME保留策略的Annotation信息的例子:

java.lang.reflect
接口 AnnotatedElement
所有已知实现类:
AccessibleObject, Class, Constructor, Field, Method, Package

表示目前正在此 VM 中运行的程序的一个已注释元素。该接口允许反射性地读取注释。由此接口中的方法返回的所有注释都是不可变并且可序列化的。调用者可以修改已赋值数组枚举成员的访问器返回的数组;这不会对其他调用者返回的数组产生任何影响。

如果此接口中的方法返回的注释(直接或间接地)包含一个已赋值的 Class 成员,该成员引用了一个在此 VM 中不可访问的类,则试图通过在返回的注释上调用相关的类返回的方法来读取该类,将导致一个 TypeNotPresentException。

isAnnotationPresent
boolean isAnnotationPresent(Class annotationClass)

如果指定类型的注释存在于此元素上,则返回 true,否则返回 false。此方法主要是为了便于访问标记注释而设计的。

参数:

annotationClass - 对应于注释类型的 Class 对象

返回:

如果指定注释类型的注释存在于此对象上,则返回 true,否则返回 false

抛出:

NullPointerException - 如果给定的注释类为 null

从以下版本开始:

1.5

getAnnotation
T getAnnotation(Class annotationClass)

如果存在该元素的指定类型的注释,则返回这些注释,否则返回 null。

参数:

annotationClass - 对应于注释类型的 Class 对象

返回:

如果该元素的指定注释类型的注释存在于此对象上,则返回这些注释,否则返回 null

抛出:

NullPointerException - 如果给定的注释类为 null

从以下版本开始:

1.5

getAnnotations
Annotation[] getAnnotations()

返回此元素上存在的所有注释。(如果此元素没有注释,则返回长度为零的数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

返回:

此元素上存在的所有注释

从以下版本开始:

1.5

getDeclaredAnnotations
Annotation[] getDeclaredAnnotations()

返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

返回:

直接存在于此元素上的所有注释

从以下版本开始:

1.5

下面是使用反射读取RUNTIME保留策略的Annotation信息的例子:

自定义注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {

    String[] value1() default "abc";
}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation(value1={"a","b"})
    @Deprecated
    public void execute(){
        System.out.println("method");
    }
}

读取注解中的信息:

public static void main(String[]args)throws SecurityException,NoSuchMethodException,IllegalArgumentException,IllegalAccessException,InvocationTargetException{
        AnnotationTest2 annotationTest2=new AnnotationTest2();

        //获取AnnotationTest2的Class实例
        Class c=AnnotationTest2.class;

//获取需要处理的方法Method实例
    Method method=c.getMethod("execute",new Class[]{});

            //判断该方法是否包含MyAnnotation注解
            if(method.isAnnotationPresent(MyAnnotation.class)){

        //获取该方法的MyAnnotation注解实例
        MyAnnotation myAnnotation=method.getAnnotation(MyAnnotation.class);

        //执行该方法
        method.invoke(annotationTest2,new Object[]{});

        //获取myAnnotation
        String[]value1=myAnnotation.value1();
        System.out.println(value1[0]);
        }

        //获取方法上的所有注解
        Annotation[]annotations=method.getAnnotations();
        for(Annotation annotation:annotations){
        System.out.println(annotation);
        }
        }

8、限定注解的使用:

限定注解使用@Target。

@Documented
@Retention(value=RUNTIME)
@Target(value=ANNOTATION_TYPE)
public @interface Target

指示注释类型所适用的程序元素的种类。如果注释类型声明中不存在 Target 元注释,则声明的类型可以用在任一程序元素上。如果存在这样的元注释,则编译器强制实施指定的使用限制。 例如,此元注释指示该声明类型是其自身,即元注释类型。它只能用在注释类型声明上:

@Target(ElementType.ANNOTATION_TYPE)
    public @interface MetaAnnotationType {
        ...
    }

此元注释指示该声明类型只可作为复杂注释类型声明中的成员类型使用。它不能直接用于注释:

@Target({}) 
    public @interface MemberType {
        ...
    }

这是一个编译时错误,它表明一个 ElementType 常量在 Target 注释中出现了不只一次。例如,以下元注释是非法的:

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
    public @interface Bogus {
        ...
    }
public enum ElementType
extends Enum

程序元素类型。此枚举类型的常量提供了 Java 程序中声明的元素的简单分类。

这些常量与 Target 元注释类型一起使用,以指定在什么情况下使用注释类型是合法的。

  • ANNOTATION_TYPE
    注释类型声明

  • CONSTRUCTOR
    构造方法声明

  • FIELD
    字段声明(包括枚举常量)

  • LOCAL_VARIABLE
    局部变量声明

  • METHOD
    方法声明

  • PACKAGE
    包声明

  • PARAMETER
    参数声明

  • TYPE
    类、接口(包括注释类型)或枚举声明

注解的使用限定的例子:

@Target(ElementType.METHOD)
public @interface MyAnnotation {

    String[] value1() default "abc";
}

9、在帮助文档中加入注解:

要想在制作JavaDoc文件的同时将注解信息加入到API文件中,可以使用java.lang.annotation.Documented。

在自定义注解中声明构建注解文档:

@Documented
public @interface MyAnnotation {

    String[] value1() default "abc";
}

使用自定义注解:

public class AnnotationTest2 {

    @MyAnnotation(value1={"a","b"})
    public void execute(){
        System.out.println("method");
    }
}

10、在注解中使用继承:

默认情况下注解并不会被继承到子类中,可以在自定义注解时加上java.lang.annotation.Inherited注解声明使用继承。

@Documented
@Retention(value=RUNTIME)
@Target(value=ANNOTATION_TYPE)
public @interface Inherited

指示注释类型被自动继承。如果在注释类型声明中存在 Inherited 元注释,并且用户在某一类声明中查询该注释类型,同时该类声明中没有此类型的注释,则将在该类的超类中自动查询该注释类型。此过程会重复进行,直到找到此类型的注释或到达了该类层次结构的顶层 (Object) 为止。如果没有超类具有该类型的注释,则查询将指示当前类没有这样的注释。

注意,如果使用注释类型注释类以外的任何事物,此元注释类型都是无效的。还要注意,此元注释仅促成从超类继承注释;对已实现接口的注释无效。

十二、说说你对依赖注入的理解?

假设你编写了两个类,一个是人(Person),一个是手机(Mobile)。
人有时候需要用手机打电话,需要用到手机的callUp方法。
传统方法中,类Person的makeCall方法对Mobile类具有依赖,必须手动生成一个新的实例new Mobile()才可以进行之后的工作。依赖注入的思想是这样,当一个类(Person)对另一个类(Mobile)有依赖时,不再该类(Person)内部对依赖的类(Moblile)进行实例化,而是之前配置一个beans.xml,告诉容器所依赖的类(Mobile),在实例化该类(Person)时,容器自动注入一个所依赖的类(Mobile)的实例。

十三、说一下泛型原理,并举例说明?

Java从1.5之后支持泛型,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

如不支持泛型,则表现为支持Object,不是特定的泛型。
泛型是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
可以在集合框架中看到泛型的动机。例如,List类允许您向一个 List添加任意类的对象,即使最常见的情况List.add()。
因为 list.get() 被定义为返回 Object,所以一般必须将 list.get() 的结果强制类型转换为期望的类型,如下面的代码所示:
List list = new ArrayList();
list .add("obj");
String s = (String) list.get(0);
要让程序通过编译,必须将 get() 的结果强制类型转换为 String,并且希望结果真的是一个 String。但是有可能某人已经在该映射中保存了不是 String 的东西,这样的话,上面的代码将会抛出 ClassCastException。
理想情况下,您可能会得出这样一个观点,即 list 是一个 List,它将 String 键映射到 String 值。
泛型可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。

十四、Java中String的了解?

1)String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。

2)String类其实是通过char数组来保存字符串的。
无论是sub操、concat还是replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
在这里要永远记住一点:“String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象”。
我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性我们可以十分肯定常量池中一定不存在两个相同的字符串(这点对理解上面至关重要)。

Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。

所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。

而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

来看下面的程序:

String a = "chenssy";
String b = "chenssy";

a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"对象,他们指向同一个对象。
String c =newString("chenssy");

new关键字一定会产生一个对象chenssy(注意这个chenssy和上面的chenssy不同),同时这个对象是存储在堆中。所以上面应该产生了两个对象:保存在栈中的c和保存堆中chenssy。但是在Java中根本就不存在两个完全一模一样的字符串对象。故堆中的chenssy应该是引用字符串常量池中chenssy。所以c、chenssy、池chenssy的关系应该是:c--->chenssy--->池chenssy。

总结:

  • 1.String类初始化后是不可变的(immutable)
    String使用private final char value[]来实现字符串的存储,也就是说String对象创建之后,就不能再修改此对象中存储的字符串内容,就是因为如此,才说String类型是不可变的(immutable)。程序员不能对已有的不可变对象进行修改。我们自己也可以创建不可变对象,只要在接口中不提供修改数据的方法就可以。
    然而,String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。比如:
    s = s.replace("World", "Universe");
    上面对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象的引用。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。

  • 2.引用变量与对象
    A aa;
    这个语句声明一个类A的引用变量aa[我们常常称之为句柄],而对象一般通过new创建。所以aa仅仅是一个引用变量,它不是对象。

  • 3.创建字符串的方式
    创建字符串的方式归纳起来有两类:
    (1)使用""引号创建字符串;
    (2)使用new关键字创建字符串。
    结合上面例子,总结如下:
    (1)单独使用""引号创建的字符串都是常量,编译期就已经确定存储到String Pool中;
    (2)使用new String("")创建的对象会存储到heap中,是运行期新创建的;
    new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则,将使得堆中的字符串永远是池中的子集,导致浪费池的空间)!
    (3)使用只包含常量的字符串连接符如"aa" + "aa"创建的也是常量,编译期就能确定,已经确定存储到String Pool中;
    (4)使用包含变量的字符串连接符如"aa" + s1创建的对象是运行期才创建的,存储在heap中;

  • 4.使用String不一定创建对象
    在执行到双引号包含字符串的语句时,如String a = "123",JVM会先到常量池里查找,如果有的话返回常量池里的这个实例的引用,否则的话创建一个新实例并置入常量池里。所以,当我们在使用诸如String str = "abc";的格式定义对象时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。

  • 5.使用new String,一定创建对象
    在执行String a = new String("123")的时候,首先走常量池的路线取到一个实例的引用,然后在堆上创建一个新的String实例,走以下构造函数给value属性赋值,然后把实例引用赋值给a:

  • 6.关于String.intern()
    intern方法使用:一个初始为空的字符串池,它由类String独自维护。当调用
    intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。

  • 7.关于equals和==
    (1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean
    ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。
    (2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等,即是否指向同一个对象。
    (3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。

  • 8.当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

  • 9.String的不可变性导致字符串变量使用+号的代价:
    String s = "a" + "b" + "c";
    String s1 = "a";
    String s2 = "b";
    String s3 = "c";
    String s4 = s1 + s2 + s3;
    分析:变量s的创建等价于 String s = "abc"; 由上面例子可知编译器进行了优化,这里只创建了一个对象。由上面的例子也可以知道s4不能在编译期进行优化,其对象创建相当于:
    StringBuilder temp =new StringBuilder();
    temp.append(a).append(b).append(c);
    String s = temp.toString();
    不难推断出String 采用连接运算符(+)效率低下原因。

  • 10.关于String str = new String("abc")创建了多少个对象?
    new只调用了一次,也就是说只创建了一个对象。而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
    因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。

  • 11.字符串池的优缺点:
    字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。

十五、String为什么要设计成不可变的?

1. 字符串常量池的需要
字符串常量池(String pool,String
intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

2. 允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 在String类的定义中有如下代码:

3. 安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。

十六、Object类的equal和hashCode方法重写,为什么?

hashCode是编译器为不同对象产生的不同整数,根据equal方法的定义:如果两个对象是相等(equal)的,那么两个对象调用hashCode必须产生相同的整数结果,即:equal为true,hashCode必须为true,equal为false,hashCode也必须为false,所以必须重写hashCode来保证与equal同步。

class Student {
    int num;
    String name;

    Student(int num, String name) {
        this.num = num;
        this.name = name;
    }

    public int hashCode() {
        return num * name.hashCode();
    }

    public boolean equals(Object o) {
        Student s = (Student) o;
        return num == s.num && name.equals(s.name);
    }

    public String toString() {
        return num + ":" + name;
    }
}

在java的集合中,判断两个对象是否相等的规则是:
1,判断两个对象的hashCode是否相等
如果不相等,认为两个对象也不相等,完毕
如果相等,转入2
2,判断两个对象用equals运算是否相等
如果不相等,认为两个对象也不相等
如果相等,认为两个对象相等

为什么equals()相等,hashCode就一定要相等,而hashCode相等,却不要求equals相等?
答案:
1、因为是按照hashCode来访问小内存块,所以hashCode必须相等。
2、HashMap获取一个对象是比较key的hashCode相等和equal为true。

之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equal为false。

为什么需要hashCode?
1、 通过hashCode可以很快的查到小内存块。
2、 通过hashCode比较比equal方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。

你可能感兴趣的:(java深入源码级的面试题)