面试

Android 面试要点之java基础篇

2018-02-24  本文已影响81人  鹿小纯0831
1. 对抽象、继承、多态的理解

抽象:表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象,在java中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类就是为了继承而存在的,如果定义了一个抽象类而不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情,用 abstract 修饰方法时,此方法就是抽象方法,抽象方法必须存在于抽象类中,抽象方法没有方法体,对于一个父类来说,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为抽象方法,抽象方法的修饰符必须为 public 或者 protected ,应为用 private,则不能被子类继承,子类便无法实现该方法,缺省情况下默认为 public 。

封装:是面向对象方法的重要原则,就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性

继承:是面向对象最显著的一个特性,继承是从已有的类中派生出新的类称为子类,子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性

多态:指允许不同的对象对同一消息做出相应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。

2. 泛型的作用及使用场景

Java从1.5之后支持泛型,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样。
泛型可以消除代码中的强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止有人将错误类型的键或值保存在集合中。这就是泛型所做的工作。

泛型的好处:

Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率

泛型在使用中还有一些规则和限制:

  1. 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
  2. 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  3. 泛型的类型参数可以有多个。
  4. 泛型的参数类型可以使用extends语句,例如<T extends superclass>。习惯上成为“有界类型”。
  5. 泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(Java.lang.String);

使用泛型最常见的就是ArrayList<String>;目前集合框架以及封装的各个模块都要使用泛型。

泛型的使用场景:
1. 通配符
在开发中对象的引用传递是最常见的,但是在泛型操作中,进行引用传递的时候泛型必须匹配才可以传递,否则无法传递。

class Info<T>{
    private T var ; // 定义泛型变量 
    
    public void setVar(T var){ 
        this.var = var ; 
    } 
    
    public T getVar(){
        return this.var ; 
    } 
    
    public String toString(){
        // 直接打印 
        return this.var.toString() ;
    } 
}; 

public class GenericsDemo14{ 
    public static void main(String args[]){
        Info<String> i = new Info<String>() ; // 使用String为泛型类型 
        i.setVar("MLDN") ; 
        // 设置内容 
        fun(i) ; 
    } 
    
    public static void fun(Info<?> temp){ 
        // 可以接收任意的泛型对象 
        System.out.println("内容:" + temp) ; 
    } 
};

使用?可以接受任意类型的数据,却无法进行修改,?为通配符。

2. 受限泛型

image
class Info<T>{
    private T var ; // 定义泛型变量 
    
    public void setVar(T var){ 
        this.var = var ; 
    } 
    
    public T getVar(){
        return this.var ; 
    } 
    
    public String toString(){
        // 直接打印 
        return this.var.toString() ;
    } 
}; 

public class GenericsDemo17 { 
    public static void main(String[] args) { 
        Info<Integer> info1 = new Info<Integer>(); // 声明Integer的泛型对象  
        Info<Float> info2 = new Info<Float>(); // 声明Float的泛型对象
        Info<String> info3 = new Info<String>(); 
        info1.setVar(30); // 设置整数,自动装箱 
        info2.setVar(30.1F); // 设置小数,自动装箱  
        info3.setVar("俺是字符串,不能被受限的FUN组装"); 
        fun(info1); 
        fun(info2); 
        // fun(info3); //受限了,不能调用这个  
    } 

    public static void fun(Info<? extends Number> temp){ 
        // 只能接收String或Object类型的泛型 
        public static void fun(Info<? super String> temp){ 
            System.out.println("内容:"+temp); 
        } 
    }
}

public class GenericsDemo21{ 
    public static void main(String args[]){ 
        Info<String> i1 = new Info<String>() ; // 声明String的泛型对象 
        Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象 
        i1.setVar("hello") ;
        i2.setVar(new Object());
        fun(i1) ; 
        fun(i2) ;
    } 
    
    public static void fun(Info<? super String> temp){
        // 只能接收String或Object类型的泛型 
        System.out.print(temp + "、") ; 
    } 
};

如果设置成Stirng类型就会出现错误:

类型参数 java.lang.String 不在其限制范围之内
Info<String> i1 = new Info<String>() ; // 声明Integer的 泛型对象
String 不是Number的子类,最高不能超过Number的子类。

3. 泛型接口
访问权限 +interface +接口名称 + <泛型标示>{}
泛型接口实现的两种方式:

interface Info<T>{   // 在接口上定义泛型
    public T getVar() ;  // 定义抽象方法,抽象方法的返回值就是泛型类型 
}

class InfoImpl implements Info<String>{ // 定义泛型接口的子类 
    private String var ; // 定义属性 
    public InfoImpl(String var){ 
        // 通过构造方法设置属性内容 
        this.setVar(var) ; 
    } 
    public void setVar(String var){ 
        this.var = var ;
    } 
    public String getVar(){ 
        return this.var ; 
    } 
};

public class GenericsDemo{ 
    public static void main(String arsg[]){ 
        Info i = null; // 声明接口对象 
        i = new InfoImpl("soyoungboy") ; // 通过子类实例化对象 
        System.out.println("内容:" + i.getVar()) ; 
    } 
};
interface Info<T>{ // 在接口上定义泛型
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型 
} 
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类 
    private T var ; // 定义属性 
    public InfoImpl(T var){ 
    // 通过构造方法设置属性内容 
    this.setVar(var) ; 
    } 
    public void setVar(T var){ 
        this.var = var ; 
    } 
    public T getVar(){ 
        return this.var ; 
    } 
}; 
public class GenericsDemo{ 
    public static void main(String arsg[]){ 
        Info<String> i = null; // 声明接口对象 
        i = new InfoImpl<String>("soyoungboy") ; // 通过子类实例化对象 
        System.out.println("内容:" + i.getVar()) ; 
    } 
};

4. 泛型方法
访问权限 +<泛型标示>+泛型标示 方法名称(泛型标示 参数名称)

class Demo{ 
    public <T> T fun(T t){ // 可以接收任意类型的数据 
        return t ; // 直接把参数返回  
    } 
}; 
public class GenericsDemo{ 
    public static void main(String args[]){ 
        Demo d = new Demo() ; // 实例化Demo对象 
        String str = d.fun("soyoungboy") ; // 传递字符串 
        int i = d.fun(30) ; // 传递数字,自动装箱 
        System.out.println(str) ; // 输出内容 
        System.out.println(i) ; // 输出内容  
    } 
};
3. 枚举的特点及使用场景

基本特性:

  1. enum关键字
    枚举enum是同class,interface同一级别的特性。
  2. 枚举的定义
    如何在Enum中定义枚举常量呢?
    1,枚举常量没有任何修饰符
    2,每个常量以“,”分隔,以“;”结束枚举常量的描述。
    3,枚举常量必须定义在所有方法或者构造器之前。
//枚举常量的定义可以在最后一个元素加,再加;结束。常量区分大小写
public enum OrdinaryEnums {
        red,
        RED,
        green,
        yellow,
        blue,;
}
  1. 使用枚举的目的
    枚举的作用不仅仅可以让你使用限制在一个enum中的变量,并且这些变量的灵活性和拓展性很好。
    只要是需要控制变量的数量或者范围,并且拿到变量后还需要处理一些逻辑的场景都可以用枚举来完成。
public enum WeekEnums {
        //注:枚举写在最前面,否则编译出错
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday,;

        private static String getWeek(WeekEnums weekEnums) {
            String week = null;
            switch (weekEnums) {
                case Sunday://星期天
                    week = "星期天";
                    break;
                case Monday://星期一
                    week = "星期一";
                    break;
                case Tuesday:// 星期二
                    week = "星期二";
                    break;
                case Wednesday://星期三
                    week = "星期三";
                    break;
                case Thursday:// 星期四
                    week = "星期四";
                    break;
                case Friday://星期五
                    week = "星期五";
                    break;
                case Saturday://  星期六
                    week = "星期六";
                    break;
            }
            return week;
        }
    }
//获取方式:
String weekday = WeekEnums.getWeek(WeekEnums.Friday);
4.线程sleep和wait的区别

线程其实没什么好说的,但凡说道线程问题,多半都是指多线程,可以参考多线程学习
1. 基本特征:
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

  1. Thread.start()方法调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。start()方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。
  2. 从程序运行的结果可以发现,多线程程序是乱序执行。
  3. Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
    实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
  4. 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

2.线程状态转换

image
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

3. 线程调度

  1. 调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
  2. 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。
  3. 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。
  4. 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  5. 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  6. 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
/** 
 * wait用法 
 * @author DreamSea  
 * @time 2015.3.9  
 */  
package com.multithread.wait;  
public class MyThreadPrinter2 implements Runnable {     
        
    private String name;     
    private Object prev;     
    private Object self;     
    
    private MyThreadPrinter2(String name, Object prev, Object self) {     
        this.name = name;     
        this.prev = prev;     
        this.self = self;     
    }     
    
    @Override    
    public void run() {     
        int count = 10;     
        while (count > 0) {     
            synchronized (prev) {     
                synchronized (self) {     
                    System.out.print(name);     
                    count--;    
                      
                    self.notify();     
                }     
                try {     
                    prev.wait();     
                } catch (InterruptedException e) {     
                    e.printStackTrace();     
                }     
            }     
    
        }     
    }     
    
    public static void main(String[] args) throws Exception {     
        Object a = new Object();     
        Object b = new Object();     
        Object c = new Object();     
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);     
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);     
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);     
             
             
        new Thread(pa).start();  
        Thread.sleep(100);  //确保按顺序A、B、C执行  
        new Thread(pb).start();  
        Thread.sleep(100);    
        new Thread(pc).start();     
        Thread.sleep(100);    
        }     
}    

输出结果:
ABCABCABCABCABCABCABCABCABCABC
先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

5. wait和sleep区别
共同点:

  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
    如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
    需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

不同点:

  1. Thread类的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
    所以sleep()和wait()方法的最大区别是:
    sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。
      但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
重要:Java中sleep()与wait()的区别总结

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。
举个例子:

/**
 * java中的sleep()和wait()的区别
 * @author Hongten
 * @date 2013-12-10
 */
public class TestD {
 
public static void main(String[] args) {
    new Thread(new Thread1()).start();
    try {
        Thread.sleep(5000);
    } catch (Exception e) {
        e.printStackTrace();
    }
    new Thread(new Thread2()).start();
}
  
private static class Thread1 implements Runnable{
    @Override
    public void run(){
        synchronized (TestD.class) {
            System.out.println("enter thread1..."); 
            System.out.println("thread1 is waiting...");
            try {
                //调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池
                TestD.class.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("thread1 is going on ....");
            System.out.println("thread1 is over!!!");
        }
    }
}
  
private static class Thread2 implements Runnable{
    @Override
    public void run(){
        synchronized (TestD.class) {
            System.out.println("enter thread2....");
            System.out.println("thread2 is sleep....");
            //只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
            TestD.class.notify();
            //==================
            //区别
            //如果我们把代码:TestD.class.notify();给注释掉,即TestD.class调用了wait()方法,但是没有调用notify()
            //方法,则线程永远处于挂起状态。
            try {
                //sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,
                //但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
                //在调用sleep()方法的过程中,线程不会释放对象锁。
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("thread2 is going on....");
            System.out.println("thread2 is over!!!");
        }
    }
}
}

运行效果:

enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
thread1 is going on ....
thread1 is over!!!

如果注释掉代码:

TestD.class.notify();

运行效果:

enter thread1...
thread1 is waiting...
enter thread2....
thread2 is sleep....
thread2 is going on....
thread2 is over!!!
5. JAVA反射机制

详情参考
什么是Java反射机制?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的以及动态调用对象的方法的功能称为Java的反射机制。
另:Java的反射机制在平时的业务开发过程中很少使用到,但是在一些基础框架的搭建上应用非常广泛。

6. weak/soft/strong引用的区别

强引用
就是new出来的,不会被垃圾回收。
软引用
当对象是Soft reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收它。因此Soft reference适合用于构建一些缓存系统,比如图片缓存。
弱引用
WeakReference被GC回收的可能性较大,在使用它之前,你需要通过weakObj.get()去判断目的对象引用是否已经被回收。

7. Object的hashCode()与equals()的区别和作用

这个要总结起来就太麻烦了,还是参考大神的吧。参考资料

HashSet和HashMap一直都是JDK中最常用的两个类,HashSet要求不能存储相同的对象,HashMap要求不能存储相同的键。 那么Java运行时环境是如何判断HashSet中相同对象、HashMap中相同键的呢?当存储了“相同的东西”之后Java运行时环境又将如何来维护呢?

在研究这个问题之前,首先说明一下JDK对equals(Object obj)和hashcode()这两个方法的定义和规范:
在Java中任何一个对象都具备equals(Object obj)和hashcode()这两个方法,因为他们是在Object类中定义的。
equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同”则返回true,否则返回false。
hashcode()方法返回一个int数,在Object类中的默认实现是“将该对象的内部地址转换成一个整数返回”。
接下来有两个个关于这两个方法的重要规范(我只是抽取了最重要的两个,其实不止两个):
规范1:若重写equals(Object obj)方法,有必要重写hashcode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashcode()返回值。说得简单点就是:“如果两个对象相同,那么他们的hashcode应该 相等”。不过请注意:这个只是规范,如果你非要写一个类让equals(Object obj)返回true而hashcode()返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了Java规范,程序也就埋下了BUG。
规范2:如果equals(Object obj)返回false,即两个对象“不相同”,并不要求对这两个对象调用hashcode()方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的hashcode可能相同”。
根据这两个规范,可以得到如下推论: ☆ ☆ ☆ ☆
1、如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
2、如果两个对象不equals,他们的hashcode有可能相等。
3、如果两个对象hashcode相等,他们不一定equals。
4、如果两个对象hashcode不相等,他们一定不equals。

这样我们就可以推断Java运行时环境是怎样判断HashSet和HastMap中的两个对象相同或不同了。我的推断是:先判断hashcode是否相等,再判断是否equals。

测试程序如下:首先我们定义一个类,重写hashCode()和equals(Object obj)方法

Java代码

class A {    
    @Override    
    public boolean equals(Object obj) {    
        System.out.println("判断equals");    
        return false;    
    }    

    @Override    
    public int hashCode() {    
        System.out.println("判断hashcode");    
        return 1;    
    }    
}  

然后写一个测试类,代码如下:

Java代码

public class Test {    
    public static void main(String[] args) {    
        Map<A,Object> map = new HashMap<A, Object>();    
        map.put(new A(), new Object());    
        map.put(new A(), new Object());    
        System.out.println(map.size());    
    }    
} 

运行之后打印结果是:

判断hashcode 
判断hashcode 
判断equals 
2 

可以看出,Java运行时环境会调用new A()这个对象的hashcode()方法。其中:
打印出的第一行“判断hashcode”是第一次map.put(new A(), new Object())所打印出的。
接下来的“判断hashcode”和“判断equals”是第二次map.put(new A(), new Object())所打印出来的。

那么为什么会是这样一个打印结果呢?我是这样分析的:
1、当第一次map.put(new A(), new Object())的时候,Java运行时环境就会判断这个map里面有没有和现在添加的 new A()对象相同的键,判断方法:调用new A()对象的hashcode()方法,判断map中当前是不是存在和new A()对象相同的HashCode。显然,这时候没有相同的,因为这个map中都还没有东西。所以这时候hashcode不相等,则没有必要再调用 equals(Object obj)方法了。参见推论4(如果两个对象hashcode不相等,他们一定不equals)
2、当第二次map.put(new A(), new Object())的时候,Java运行时环境再次判断,这时候发现了map中有两个相同的hashcode(因为我重写了A类的hashcode()方 法永远都返回1),所以有必要调用equals(Object obj)方法进行判断了。参见推论3(如果两个对象hashcode相等,他们不一定equals),然后发现两个对象不equals(因为我重写了equals(Object obj)方法,永远都返回false)。
3、这时候判断结束,判断结果:两次存入的对象不是相同的对象。所以最后打印map的长度的时候显示结果是:2。

改写程序如下:
Java代码

class A {    
    @Override    
    public boolean equals(Object obj) {    
        System.out.println("判断equals");    
        return true;    
    }    

    @Override    
    public int hashCode() {    
        System.out.println("判断hashcode");    
        return 1;    
    }    
}    

public class Test {    
    public static void main(String[] args) {    
    Map<A,Object> map = new HashMap<A, Object>();    
    map.put(new A(), new Object());    
    map.put(new A(), new Object());    
    System.out.println(map.size());    
    }    
} 

运行之后打印结果是:

 判断hashcode 
判断hashcode 
判断equals 
1 

显然这时候map的长度已经变成1了,因为Java运行时环境认为存入了两个相同的对象。原因可根据上述分析方式进行分析。

以上分析的是HashMap,其实HashSet的底层本身就是通过HashMap来实现的,所以他的判断原理和HashMap是一样的,也是先判断hashcode再判断equals。

所以:写程序的时候应尽可能的按规范来,不然在不知不觉中就埋下了bug!

上一篇下一篇

猜你喜欢

热点阅读