首页 > 技术文章 > Java基础面试相关

sq-bmw 2019-12-12 12:55 原文

面试相关的问题(上)

一 TransferValue 传值

为什么 Java 中只有值传递?

首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。

Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

方法位于JVM的栈中

代码示例

首先需要一个Person类

package com.bao;

/**
 * @auther Bao
 * @date 2019/11/26 23:02
 */
public class Person {
    private String personName;

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

    public Person() {
    }

    public String getPersonName() {
        return personName;
    }

    public void setPersonName(String personName) {
        this.personName = personName;
    }
}

TestTransferValue 测试类

package com.bao;

/**
 * @auther Bao
 * @date 2019/11/26 22:58
 */
public class TestTransferValue {
    public void changeValue1( int age){
        age=30;
    }
    public void changeValue2( Person person){
        person.setPersonName("xxx");
    }
    public void changeValue3( String str){
        str ="xxx";
    }

    public static void main(String[] args) {
        TestTransferValue test =new TestTransferValue();
        int age= 20;
        test.changeValue1(age);
        System.out.println("age ===>:"+age);

        Person person =new Person("abc");
        test.changeValue2(person);
        System.out.println("personName===>:"+person.getPersonName());

        String str="abc";
        test.changeValue3(str);
        System.out.println("String ===>:"+str);
    }
}

运行结果

age ===>:20
personName===>:xxx
String ===>:abc

结果分析
1.基本思想

基本类型,传复印件,原件不动

引用类型传的是内存地址

2.在上面的代码中可以看到,在main方法中的age与changeValue1(int age)传的age并不是同一个,可以理解为是一个复本,并不是原件,所以main方法中打印的age是原件,并没有改动,因此打印的为 age ===>:20.

3.引用类型传的是地址,main方法中的person,被changeValue2的person(引用过来的,指向同一个地址)由"abc"改为"xxx",地址不变,只不过是值发生了变化.再出栈回到main方法中,person依旧指着这个地址,现在这个地址的值是"xxx",所以输出的是"xxx".

面试技巧:基本类型传值不变,引用类型传值发生改变

4.String str="abc";位于字符串常量池中,在堆中的元空间

原则是 有,就直接复用,没有,就在常量池中新建

main方法中的str="abc"发现在常量池中没有,所以新建了一个,再到changeValue3中(str传的是main方法中的引用,也就是内存地址),str=xxx,发现常量池中没有xxx,就重新在新的地址创建了字符串xxx,指针也随之指向了新的地址.再回到main方法中,他的str(注意作用域)指向内存中的地址并没有发生改变,

所以输出的是String ===>:abc

二 equals和==

代码示例1

package com.bao;

import java.util.HashSet;

/**
 * @auther Bao
 * @date 2019/12/8 20:23
 */
public class TestEquals {
    public static void main(String[] args) {
        String str1 = new String("abc");
        String str2 = new String("abc");
        System.out.println(str1 == str2);
        System.out.println(str1.equals(str2));
        System.out.println("=====================分割线=========================");
        HashSet<String> set_01 = new HashSet<>();
        set_01.add(str1);
        set_01.add(str2);
        System.out.println(set_01.size());
        System.out.println("=====================分割线=========================");
        Person_01 person_01 = new Person_01("abc");
        Person_01 person_02 = new Person_01("abc");
        System.out.println(person_01 == person_02);
        System.out.println(person_01.equals(person_02));
        System.out.println("=====================分割线=========================");
        HashSet<Person_01> set_02 = new HashSet<>();
        set_02.add(person_01);
        set_02.add(person_02);
        System.out.println(set_02.size());
    }

}

输出结果

false
true
=====================分割线=========================
1
=====================分割线=========================
false
false
=====================分割线=========================
2

分析

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象

(基本数据类型== 比较的是值,引用数据类型==比较的是内存地址)。

引用类型new出来的对象,用==比较都为false

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。比较的是内存地址
  • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

说明:

  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较

== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;

而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等

回到代码

1.String类型

str1.equals(str2)

这个为String类型,我们点equals.调到的是String类,发现String类重写了equals方法

String中equals方法

    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;
    }

2.引用类型

person_01.equals(person_02)

这个引用类型的equals,没有被重写过

//源码中跳到的是Object类,实质为 == 比较
public boolean equals(Object obj) {
    return (this == obj);
}

3.hashset

 HashSet<String> set_01 = new HashSet<>();

hashset的父接口为set

数据接口中没有HashSet,他的底层数据结构是hashmap

public HashSet() {
    map = new HashMap<>();
}

在源码中我们发现,hashset的add方法,调用的是hashmap的put方法

public boolean add(E e) {  
    //put方法的key(e)就是set的对象,value(PRESENT)是一个Object常量
    return map.put(e, PRESENT)==null;
}

那么HashSet怎么判断是不是同一个对象呢?

那就是靠hashcode的值

HashSet : 无序,无重复

ArrayList : 有序,有重复

String类也同样覆写了hashcode方法

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;
}

回到代码

HashSet<String> set_01 = new HashSet<>();
set_01.add(str1);
set_01.add(str2);
System.out.println(set_01.size());

String类型的str1 和 str2的hashcode相同,所以在HashSet中添加,是同一个对象,所以set_01.size()只有一个..

Person_01类的hashcode为Object类的hashcode

HashSet<Person_01> set_02 = new HashSet<>();
set_02.add(person_01);
set_02.add(person_02);
System.out.println(set_02.size());
public native int hashCode();

person_01和person_02的hashCode并不相同,所以在HashSet中添加,不是一个对象,所以set_02.size()

的值为2个

hashCode 与 equals (重要)

面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”

hashCode()介绍

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有 hashCode

我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

通过我们可以看出:hashCode() 的作用就是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

hashCode()与equals()的相关规定

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,对两个对象分别调用equals方法都返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的(不同的对象也可能产生相同的hashcode,概率性问题)
  4. 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
  5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

代码示例2

package com.bao;

/**
 * @auther Bao
 * @date 2019/12/8 21:50
 */
public class TestEquals_02 {
    public static void main(String[] args) {
        String s1="abc";
        String s2=new String("abc");
        String s3="abc";
        String s4="xxx";
        String s5="abc"+"xxx";
        String s6=s3+s4;

        System.out.println(s1 == s2);
        System.out.println(s1 == s5);
        System.out.println(s1 == s6);
        System.out.println(s5 == s6);

        System.out.println(s1 == s6.intern());
        System.out.println(s2 == s2.intern());
    }
}

输出结果

false
false
false
false
false
false

结果分析

//s1与s2不是同一个对象(地址不一样),所以结果为false
String s1="abc"; //位于元空间中的常量池
String s2=new String("abc"); //new出的对象.在堆中的Eden区

intern()方法的源码

public native String intern();

方法注释的解释

也就是说,intern方法的意思为,直接去字符常量池中去找,找到了就用,找不到就新建

所以

//s1与s6不是同一个
System.out.println(s1 == s6.intern());//false
//s2位于堆中(new出来的),s2.intern()是在常量池中
System.out.println(s2 == s2.intern());//false

三 CodeBlock(代码块)

类的加载机制

代码示例1

package com.bao.code;

/**
 * @auther Bao
 * @date 2019/12/8 22:30
 */
public class CodeBlock_01 {
    public static void main(String[] args) {
        {
            int x= 11;
            System.out.println("普通代码块内的变量 x = "+x);
        }

        {
            int y= 13;
            System.out.println("普通代码块内的变量 y = "+y);
        }
        int x= 12;
        System.out.println("main方法内的变量 x = "+x);
    }
    static {

        System.out.println("静态代码块 ");
    }
    static {
        int x= 14;
        System.out.println("静态代码块内的变量 x = "+x);
    }

}

普通代码块 : 在方法或语句中出现的 {}就称为普通代码块,普通代码块和一般的语句执行顺序由他们在代码块中出现的次序决定--->"先出现先执行"

静态代码块 : 类加载的时候,就会加载,出现的顺序就是执行的顺序

输出结果

静态代码块 
静态代码块内的变量 x = 14
普通代码块内的变量 x = 11
普通代码块内的变量 y = 13
main方法内的变量 x = 12

代码示例2

package com.bao.code;

/**
 * @auther Bao
 * @date 2019/12/8 22:43
 */
public class CodeBlock_02 {
    {
        System.out.println("第二构造块 3 ");
    }
    public CodeBlock_02(){
        System.out.println("构造方法 2");
    }
    {
        System.out.println("第一代码块 1 ");
    }

    public static void main(String[] args) {
        new CodeBlock_02();
        System.out.println("================");
        new CodeBlock_02();
    }
}

输出结果

第二构造块 3 
第一代码块 1 
构造方法 2
================
第二构造块 3 
第一代码块 1 
构造方法 2

构造代码块在每次创建对象时,都会被调用

构造代码块加载顺序高于构造方法

代码示例3

package com.bao.code;

/**
 * @auther Bao
 * @date 2019/12/9 12:36
 */
class Code {
    public Code() {
        System.out.println("Code的构造方法111");
    }

    {
        System.out.println("Code的构造块222");
    }

    static {
        System.out.println("Code的静态代码块333");
    }
}

public class CodeBlock_03 {
    {
        System.out.println("CodeBlock_03的构造块444");
    }

    static {
        System.out.println("CodeBlock_03的静态代码块555");
    }
    public CodeBlock_03(){
        System.out.println("CodeBlock_03的构造方法666");
    }

    public static void main(String[] args) {
        System.out.println("=========分割线=======CodeBlock_03的main方法777===========");
        new Code();
        System.out.println("----------------------------------------------------");
        new Code();
        System.out.println("----------------------------------------------------");
        new CodeBlock_03();
    }
}

输出结果

CodeBlock_03的静态代码块555
================分割线=======CodeBlock_03的main方法777===========
Code的静态代码块333
Code的构造块222
Code的构造方法111
----------------------------------------------------
Code的构造块222
Code的构造方法111
----------------------------------------------------
CodeBlock_03的构造块444
CodeBlock_03的构造方法666

结果分析

在Java中,类的加载机制

先通过javac编译产生class文件,静态的东西随着类加载而加载(只加载一次)

普通代码块的加载,只要new对象都会加载

加载顺序:静态代码块 > 普通代码块 > 构造方法

TestStaticSeq_

代码示例

package com.bao.code;

/**
 * @auther Bao
 * @date 2019/12/9 13:34
 */
class Father{
    public Father(){
        System.out.println("Father 构造方法111");
    }
    {
        System.out.println("Father 构造块222");
    }
    static{
        System.out.println("Father 静态代码块333");
    }
}
class Son extends Father{
    public Son(){
        System.out.println("Son 构造方法444");
    }
    {
        System.out.println("Son 构造块555");
    }
    static{
        System.out.println("Son 静态代码块666");
    }
}
public class TestStaticSeq {
    static {
        System.out.println("主类的静态代码块 888");
    }
    public static void main(String[] args) {
        System.out.println("main方法 === 777 ===");
        new Son();
        System.out.println("-----------------------------");
        new Son();
        System.out.println("-----------------------------");
        new Father();
    }
}

输出结果

主类的静态代码块 888
main方法 === 777 ===
Father 静态代码块333
Son 静态代码块666
Father 构造块222
Father 构造方法111
Son 构造块555
Son 构造方法444
-----------------------------
Father 构造块222
Father 构造方法111
Son 构造块555
Son 构造方法444
-----------------------------
Father 构造块222
Father 构造方法111

结果分析

首先分清楚哪个是主类,主类是编译生成的class文件,先加载主类的静态代码块,然后才是main方法

继承关系的类加载

  • 先加载的是静态代码块(只加载一次)
    • 父类静态代码块
    • 子类静态代码块
  • 在Java中,类的无参构造方法中,有一个隐式的调用,super();子类默认会调用父类的无参构造方法
    • 加载顺序为 : 从父到子,静态先行

推荐阅读