Java面试题

Interview / 2020-08-21

浅拷贝和深拷贝

在对对象进行拷贝时,比如clone方法,对基本类型的成员变量的拷贝都是复制一个新的成员变量,生成一个新的内存空间,但是对对象的引用进行拷贝时有两种情况。

第一种情况,如果只是新建了一个引用指向原来的对象内存空间,这是两个对象的这个成员引用指向同一个内存地址,这种拷贝成为浅拷贝,这是两个对象的这个成员引用其实指向的是同一个内存地址,一个对象的成员改变另一个对象的这个成员也会改变。

第二种情况,如果拷贝的时候新建了一个新的内存空间,并且把原来对象复制到了新的内存空间中,这种拷贝成为深拷贝,这时两个对象的成员互不干扰。

通过实现实现Cloneable接口的clone方法实现浅拷贝,默认就是浅拷贝。

package cn.redarm;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(11, new Person("redarm"));

        Student student2 = (Student)student1.clone();
        System.out.println("student1: " + student1.toString());
        System.out.println("student2: " + student2.toString());

        student1.person.name = "new redarm";

        System.out.println("new student1: " + student1.toString());
        System.out.println("new student2: " + student2.toString());
    }
}

class Student implements Cloneable{
    int age;
    Person person;

    public Student(int age, Person person){
        this.age = age;
        this.person = person;
    }

    public Student(){
        age = 0;
        person = new Person();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("name: ");
        sb.append(this.person.name);
        sb.append("  age: ");
        sb.append(this.age);
        return sb.toString();
    }
}

class Person{
    String name;

    public Person(){
        name = null;
    }

    public Person(String name){
        this.name = name;
    }
}


深拷贝,重写clone方法时把成员也通过clone方法拷贝一遍。

package cn.redarm;

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
        Student student1 = new Student(11, new Person("redarm"));

        Student student2 = (Student)student1.clone();
        System.out.println("student1: " + student1.toString());
        System.out.println("student2: " + student2.toString());

        student1.person.name = "new redarm";

        System.out.println("new student1: " + student1.toString());
        System.out.println("new student2: " + student2.toString());
    }
}

class Student implements Cloneable{
    int age;
    Person person;

    public Student(int age, Person person){
        this.age = age;
        this.person = person;
    }

    public Student(){
        age = 0;
        person = new Person();
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.person = (Person)this.person.clone();

        return student;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("name: ");
        sb.append(this.person.name);
        sb.append("  age: ");
        sb.append(this.age);
        return sb.toString();
    }
}

class Person implements Cloneable{
    String name;

    public Person(){
        name = null;
    }

    public Person(String name){
        this.name = name;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

也可以通过反序列化进行深拷贝

新建对象的四种方法

  • new
  • 反射
  • clone
  • 反序列化

四种引用

  • 强引用:new一个对象就是强引用,即使内存不足也不会回收对象
  • 软引用:SoftReference studentSoftReference = new SoftReference<>(new Student()); 如果内存不足就会回收软引用
  • 弱引用:WeakReference studentWeakReference = new WeakReference<>(new Student()); 如果这个引用不会再用到了就会被JVM回收
  • 虚引用:跟弱引用差不多,但是是在回收之前放到ReferenceQueue中,其他引用都是回收之后放到ReferenceQueue中的。

hashcode

比如HashSet中的元素不能重复,如果其中用1000中元素了,现在放第1001个元素,就需要拿这个元素和之前的1000个元素进行比较,需要比较1000次,如果都不相等,就可以放进去,比较时使用equal方法进行比较,这样非常耗时间。

采用hashcode,任何元素都能通过哈希函数算出他的hashcode,同时这个hashcode对应一个内存地址,这样放第1001个元素的时候算出他的hashcode,然后这个hashcode对应一个内存地址,如果这个地址中没有元素就说明所有对象中没有与这个对象相等的,就可以放进去,如果这个hashcode对应的内存地址已经有对象了,这时候使用equal方法判断两个对象是否相等,如果相等就不放进去,如果两个对象相等,就是发生了哈希碰撞,两个不同元素的hashcode一样,这种情况HashMap使用的方法就是在每个数组不是存一个元素,而是存一个链表,这样发生了哈希碰撞的元素都放到一个链表中,查找的时候再通过equal方法查找一个链表中的元素就行,因为发生碰撞的概率非常低,所以使用equal方法的次数很少,效率高。

String StringBuilder StringBuffer

String对象中成员是final的,所以是不可变的,当你在使用 + 号对String字符串进行相加的时候其实是隐式的调用了StringBuilder的append方法处理,效率更高。

StringBuilder线程不安全,StringBuffer使用了synchronized关键字保证了线程安全,但是效率比StringBuilder低。

java基本数据类型

  • 整形:int,byte,long,short
  • 字符型:char
  • 浮点型:float,double
  • 布尔型:boolean

instanceof

严格的说是双目运算符,判断一个对象是否是一个类或者接口的对象或者实现或者是间接的实现。

拆装箱

装箱:int -> Integer,调用Integer.valueOf方法

拆箱:Integer -> int,调用int数据的intValue方法

再使用Integer的时候,如果数据是在-128~127之间的时候,Integer不会在堆中新建一个数据,而是会从缓存中找。所以两个值为100的Integer对象其实是一个对象。

float s = 3.14对么

不对,java浮点型默认是双精度的,在后面加一个f或者强制类型转换成双精度。

a=a+1; a+=1;区别

+=会隐式强制类型转换,如果a为char,short左面就会出错,在运算的时候会自动提升为int类型,然后把结果int类型赋值给char或者short就会报错,+=不会报错,他会把右面的运算结果自动类型转换成左面需要的数据类型。

Java内存泄漏

会存在隐藏的内存问题,比如如果是一个强引用对象,即使这个对象没有用了,它也不会被gc回收。

重写equal方法就一定要重写hashcode方法

java hashcode方法返回对象的哈希码,这个哈希码的主要总用就是在哈希表中使用,比如HashMap,HashSet。

hashcode默认返回根据对象地址生成的哈希码,所以默认只要是new两个对象,那这两个对象的hashcode就是不一样的。

比如说HashSet,其中的元素不能重复,如何判断两个对象是否相等就是看equal方法结果,向一个有1000个对象的哈希表中添加一个对象,就需要用这个对象跟里面的所有的1000个对象作比较,这样就很慢。

HashSet是以对象的hashcode作为地址存储对象的,每次添加一个对象就算出这个对象的hashcode,然后查找这个hashcode的地址是否有对象没有对象就放进去,如果有对象可能的一种情况就是发生了哈希碰撞,就是两个不一样的对象的hashcode一样,所以如果这个位置有对象不能直接确定对象重复,需要调用equal方法判断对象是否相等。

  • 一样的对象hashcode一定一样
  • 不一样的对象hashcode可能相等
  • 重写equal方法后要让一样的对象一定返回为true

所以如果没有重写hashcode方法,那么hashcode返回的是地址的哈希码,那么新生成两个相等的对象他们的哈希码也是不一样的。那么就可以同时存在HashSet中,hashcode不同就直接加进去,不会调用equal方法判断两个对象。

测试:

package cn.redarm;

import java.util.HashSet;
import java.util.Set;

public class Main {

    public static void main(String[] args) {

        Person person = new Man("liu");
        Person person1 = new Woman("liu");
        Set<Person> set = new HashSet<>();
        set.add(person);
        set.add(person1);
        System.out.println(set.size());

    }

    private static void sayHello(Person person){
        if (person instanceof Man){
            person.sayHello();
            System.out.println("true man");
        } else {
            person.sayHello();
            System.out.println("just woman");
        }
    }
}

abstract class Person{
    public String name;

    public abstract void sayHello();

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person){
            Person person = (Person)obj;
            return this.name == person.name;
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

class Man extends Person{

    public Man(){

    }

    public Man(String name){
        super.name = name;
    }

    @Override
    public void sayHello() {
        System.out.println("I am a man.");
    }

}


class Woman extends Person{

    public Woman(){

    }

    public Woman(String name){
        super.name = name;
    }

    @Override
    public void sayHello() {
        System.out.println("I am a woman.");
    }

}


如果把重写的hashcode去掉,返回结果就是2,两个相同的元素就同时存在HashSet中了。

java 值传参 和 引用传参

  • 值传递:方法参数接受到的是对象的拷贝,在方法中改变参数的值不会改变实际对象的值。
  • 引用传递:方法参数接受的是对象的引用,改变引用就是改变对象本身。

java中基本类型属于值传递,对象属于引用传递

package cn.redarm;

public class Main {

    public static void main(String[] args) {
        Person person = new Person("liu");
        System.out.println("name= " + person.name);
        changeSuccess(person);
        System.out.println("name= " + person.name);
        int[] array = new int[2];
        array[0] = 0;
        System.out.println("array[0]= " + array[0]);
        changeSuccess(array);
        System.out.println("array[0]= " + array[0]);
        int a = 1;
        System.out.println("a= " + a);
        changeFail(a);
        System.out.println("a= " + a);
    }

    private static void changeSuccess(Object o){
        if (o instanceof Person){
            ((Person) o).name = "jiang";
        } else if (o instanceof int[]){
            ((int[])o)[0] = 1;
        }
    }

    private static void changeFail(int a){
        a = 2;
    }
}

class Person{
    String name;

    public Person(){

    }

    public Person(String name){
        this.name = name;
    }
}

对象和数组(数组其实也是对象)属于引用传参,方法参数引用的指向的对象就是实际的对象,改变参数就是改变实际的对象,基本类型的int改变参数外面的实际对象并不改变。String对象是不可改变的,所以参数的值不会被改变。