Java面试常见问题-part1

如有问题,敬请指正!

1. java中 == 和 equals 和 hashCode 的区别

  1. ==

    1. 作用:比较两个操作数的关系,返回一个 boolean 值
    2. 具体表现:如果两个操作数是基本数据类型(8种),比较值是否相等;如果操作数是引用类型,比较的是内存地址是否相同。
  2. equals

    1. Object 类方法
    2. 作用:比较引用地址是否相同,在 Object 类中的实现为
    public boolean equals(Object obj) {
            return (this == obj);
    }
    
    1. 很多类比如 String、Integer等都是重写了 equals 方法。因为 Object 类中的 equals 方法只是对两个引用是否指向同一块内存区域进行判断,但是在实际情况中,我们往往需要得到的是两个对象在逻辑上是否相等,如
    String s1 = "abc";
    String s2 = "abc";
    // 实际上对象 s1 和对象 s2 指向的是不一样的地址空间
    // 但是我们通常从逻辑上认为 s1 和 s2 是相等的,这种情况下就要重写 equals 方法,如下
    
    // java.lang.String
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
    
    1. 底层还是通过 == 来判断引用地址是否相同
  3. hashCode

    1. Object 类的本地方法,返回一个对象的 hash 值,用于快速对对象进行区分,分组
    public native int hashCode();
    
    1. 很多类都重写了 hashCode 方法
    // java.lang.String
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
    
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    // 为什么使用 31
    // 一方面:产生更分散的散列,即不同字符串 hash 值一般也不同
    // 另外  :计算效率更高,31*h 与 32 * h - h 即(h << 5) - h 等价,
    // 可以使用更高效率的移位和减法操作代替乘法操作
    
  4. 区别与联系

    1. equals 底层还是使用 = = 来进行引用地址的判断, = =还能用于基本类型操作数的值的判断;
    2. 当出现这样一种情况,需要多次比较对象的引用值是否相等,比如说在一个 Set 集合中,每次放入一个对象,都需要与之前的对象进行比较以排除重复元素,如果每次都是调用 equals 方法来进行比较,那么效率十分低下。在这种情况下,hashCode 就派上用场了。解决方式就是: 每次需要对比的时候,首先调用 hashCode() 去计算出一个 hash 值,然后根据该 hash 值来得到一个存放当前对象的地址,如果这个地址没有其他对象存在,那么直接把这个对象添加进去。如果已经有一个对象了,那么继续使用 equals 方法进行比较,如果返回 false,认为该集合中不存在这个对象,再进行一次散列,将该对象放入散列后计算出来的新地址里面,如果 equals 返回 true,则该 Set 里面已经有这个对象了,就不用再添加了。
    3. 在每个重写了 equals 方法的类中,都要重写 hashCode 方法。
    4. 如果两个对象通过 equals 方法比较相等,那么他的 hashCode 一定相等;
    5. 如果两个对象通过 hashCode 方法比较相等,那么他的 equals 不一定相等(hash 碰撞);

2. int 与 Integer 的区别

  1. int 是基本数据类型(8种),Integer 是 int 对应的包装类。这个包装类内部有一个实例变量,用于存储基本类型的值,这个类还有一些静态方法、静态变量和实例方法,以便对数据进行操作。
  2. 每种包装类都有一个静态方法valueOf() 接受基本类型,返回引用类型,也都有一个实例方法xxxValue() 返回对应的基本类型。
  3. 推荐使用静态的valueOf() 创建对象,new 每次都会创建一个新对象,但是除了 Float 和 Double 外的其他包装类,都会缓存包装类对象,减少需要创建对象的次数,节省空间,提升性能。
  4. 所有包装类的共同点
    1. 重写 Object 的方法
      1. equals 和 hashCode
    2. 都实现了 Comparable 接口
    3. 包装类和String
      1. int a = Integer.parseInt("1") :根据字符串返回基本类型的值
      2. String a = Integer.toString(1) :根据基本类型值返回字符串形式
    4. 常用变量
      1. Integer.MAX_VALUE
      2. Double.POSITIVE_INFINITY
    5. Number
      1. 这是6种数值包装类的共同父类,包装类可以返回任意的基本数值类型
    6. 不可变性
      1. 所有包装类都声明为 final,不能被继承
      2. 内部基本类型都是私有的,且声明为 final
      3. 没有定义 setter 方法
      4. 不可变使得程序变的简单安全

3. 抽象类的意义

  1. 没有具体对象对应的类,如动物类、水果类;

  2. public abstract class Animal {
        // 其他代码
        public abstract void eat();	// 动物的抽象的动作
    }
    
  3. 定义了抽象方法的类必须被声明为抽象类;抽象类可以没有抽象方法。

  4. 抽象类和具体类一样可以定义具体方法、实例变量等,但不能使用 new 创建对象。只能用他的具体子类来创建对象。

  5. 具体子类继承抽象类后必须重写抽象类的抽象方法。

  6. 抽象类的意义

    1. 为了继承使用;
    2. 引入抽象方法和抽象类,对于一些类和方法,引导使用者正确使用他们,减少误用。告诉子类一定要去实现父类的某一些方法。使用抽象类,类的使用者创建对象的时候就知道必须要使用某个具体子类,而不可能误用不完整的父类。

4. 接口和抽象类的区别

  1. 两者经常配合使用,接口定义能力,抽象类提供默认实现
  2. 抽象类和接口都不能用于创建对象,接口中的方法其实都是抽象方法;
  3. 接口中不能定义实例变量,抽象类可以,一个类可以实现多个接口,但只能继承一个父类;
  4. 一个接口经常有一个对应的抽象类;
  5. 实现接口,要重写接口的所有方法,除非实现类是抽象类;继承抽象父类,可以有选择的重写需要的方法;
  6. java 9版本中,接口的内容
    1. 成员变量是常量,public static final 数据类型 常量名称 = 数据值;
    2. 从 java 8 开始,接口中允许定义默认方法,引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能,public default 返回值类型 方法名(参数列表);
    3. 从 java 8 开始,接口中允许定义静态方法public static 返回值类型 方法名(参数列表){方法体};
    4. 从 java 9 开始允许定义私有方法,方便多个静态或默认方法复用代码
      1. 普通私有方法,private 返回值类型 方法名(参数列表){方法体};
      2. 静态私有方法,private static 返回值类型 方法名(参数列表){方法体};
    5. 接口中不能有静态代码块,也没有构造方法

5. 能否创建一个包含可变对象的不可变对象?

​ 可以创建一个包含可变对象的不可变对象。只要确保不要共享可变对象的引用,确保该引用不会被修改,当需要改变这个可变对象时,就返回原对象的一个拷贝。

6. 谈谈对 java 多态的理解

  1. 面向对象的三大基本特性:封装、继承和多态,多态也称为:动态绑定、后期绑定或运行时绑定

    1. 封装: 将对象的实现细节隐藏起来,然后通过公用方法来暴露该对象的功能;
    2. 继承: 实现软件复用的基本手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法(父类 private 修饰的方法和属性除外)。
    3. 多态: 子类对象可以直接赋值给父类变量,但运行时依然表现出子类的行为特征,意味着同一类型的对象在执行过程中可以表现出不同的行为特征。
  2. 引用变量:

    1. 编译时类型: 由声明该变量时的类型决定
    2. 运行时类型: 实际赋给该变量的对象决定
    3. 二者不一致,就可能出现多态
    Father f = new Son();
    // 引用变量 f 的编译时类型为 Father,运行时类型为 Son
    
  3. 多态:编译看父类,运行看子类。

    1. 引用变量只能调用声明该变量时所使用的类中的方法,因为编译不通过,也就不存在运行了。
    2. 因为子类继承父类之后,可能扩充了自己的方法,而父类没有这些扩充的方法,所以使用父类来声明的引用变量就无法调取这些由子类扩写的方法。与方法不同,对象的实例变量不具备多态性,因此还是使用父类的实例变量。
    public class Test {
        public static void main(String[] args) {
            Father f = new Son();
            f.father(); // 输出 “我是儿子,继承自Father。”
            System.out.println(f.fatherSay); // 输出 “我是爸爸” 而不是 “我是儿子”
            //f.eat(); // 编译器警告: Cant resolve method "eat()"
            // 如果使用类型转换
            ((Son) f).eat();	// 输出 “我爸没有这个方法”
        }
    }
    class Father {
        public String fatherSay = "我是爸爸";
        public void father() {
            System.out.println("我是爸爸");
        }
    }
    class Son extends Father {
        // 编译器提示  Filed 'fatherSay' never used 
        public String fatherSay = "我是儿子";
        @Override
        public void father() {
            System.out.println("我是儿子,继承自Father。");
        }
        public void eat() {
            System.out.println("我爸没有这个方法。");
        }
    }
    

7. String、StringBuffer、StringBuilder 区别

  1. 都是 final 类,都不能被继承

  2. String 不可变,StringBuffer 和 StringBuilder 可变。任何对于 String 的改变都会产生新的对象,然后将指针指向新的对象,而对于 StringBuilder 和 StringBuffer 所指向对象的改变都不会产生新的对象。

  3. StringBuilder 是线程不安全的,StringBuffer 是线程安全的,支持并发操作。单线程环境选择 StringBuilder效率更高,多线程环境选择 StringBuffer 更安全。

  4. String 类型的对象创建后会在内存中被放在一个叫做字符串常量池的共享区域。当通过常量形式使用一个字符串时,使用的就是常量池中的那个对应的 String 类型的对象。

    String hh = new String(new char[] {'h','e','l','l','o'});
    String s1 = hh;
    String s2 = hh;
    System.out.println( s1 == s2 );	// 输出 true
    
    
  5. String 可以直接使用 + 和 += 运算符,java 底层编译器一般会转换成 StringBuilder,通过 append 方法实现上述操作。

8. 泛型中 extends 和 super 的区别

  1. 都是泛型中的通配符;

  2. extends 指定通配符的上限(协变:只出不进):指定通配符上限的集合,只能从集合中取元素,取出的元素总是上限的类型,不能添加,因为编译器无法确定集合元素是哪种子类型

    List<? extends Shape> list;	// 表泛型形参必须是 Shape 的子类
    
    
  3. super 指定通配符的下限(逆变:只进不出):只添加元素,取出的元素当 Object 类型处理,可能要强制转换,不能取

    List<? super Bar> list;	// 泛型形参是 Bar 的父类
    
    

9. 进程和线程的区别

​ 两者是不同的操作系统资源管理方式。

  1. 进程:资源分配的基本单位
  2. 线程:程序执行的基本单位
  3. 协程:又称微线程,“子程序就是协程以一种特例”。
    1. 执行效率高
    2. 不需要多线程的锁机制
  4. 区别
    1. 拥有资源
      1. 进程拥有独立的地址空间
      2. 线程只是一个进程中的不同的执行路径
      3. 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
    2. 调度
    3. 系统开销
      1. 线程执行开销小,不利于资源的管理和保护
      2. 进程相反
      3. CPU 切换线程比切换进程开销小
    4. 通信方面
      1. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)

10.final,finally,finalize的区别

  1. final 修饰一个类或者方法,表示该类或者方法不允许扩展。修饰类代表这个类不能被继承,修饰方法代表这个方法不能被子类重写。修饰变量表示这个变量是一个常量。

    public final class String extends Object implements Serializable, Comparable<String>, CharSequence
    public final void say() {}
    public static final int MAX_VALUE = 0x7fffffff;
    
    
  2. finally:用在 Java 异常体系中用于关闭资源

  3. finalize:有些面向对象的程序设计语言,如 C++,有显示的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码,在析构器中,最常见的操作是回收分配给对象的存储空间。Java 有自动垃圾回收器,所以不支持析构器。

    1. 但,当对象使用了内存之外的一些其他资源,当这个资源不再需要时,将其回收和再利用就很重要;
    2. 可以为任何一个类添加 finalize 方法,该方法将会在垃圾回收器清除对象之前调用,不用过分依赖。

如有问题,敬请指正!

发布了16 篇原创文章 · 获赞 2 · 访问量 1280

猜你喜欢

转载自blog.csdn.net/yx185/article/details/103281492