Java面经——SE基础

/ Java面经 / 0 条评论 / 3558浏览

面试Java必定会问到SE部分的基础知识,我也被问过很多次,这篇文章记录一些常问的问题和答案。

一、理解JDK、JRE、JVM

所有Java开发和运行环境不一定都是同一个厂商提供的,大部分是Sun公司(现被Oracle收购)提供的,JDK、JRE、JVM等都有不同的实现。比如JDK有Oracle JDK、Open JDK以及其他公司提供的JDK等,JVM有Sun HotSpot VM、IBM J9 VM、Google Android Dalvik VM以及其他VM等。一般我们使用的是Oracle JDK + HotSpot VM。

二、重载和重写

私有(private)方法和构造方法无法被重写,但是可以被重载,一个类可以有多个被重载的构造方法。

三、Java的三大特性

四、接口(interface)和抽象类(abstract)的区别

五、==和equals区别

==:如果比较的是基本数据类型,判断其值是否相等;如果比较的是引用类型,判断两个对象的地址是否相等。

equals:equals是在Object类中定义的方法,在Object类中仅比较两个对象的地址是否相同。

public boolean equals (Object x){
    return this == x;
}

大家都知道所有类都是Object的子类,所以可以选择是否重写equals方法,以String类的equals方法为例,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;
}

在重写equals方法时需要满足几点规则:

equals方法用于判断两个对象在实际意义上是否是同一个对象,比如有两张照片判断其中的人是否是同一个人,虽然两张中的穿着、所在环境都不一样,但是在实际意义上是同一个人。equals方法常常和hashCode()一起使用。

六、hashCode和equals

equals方法上面有介绍,hashCode()定义于Object类中,该方法用于获取哈希散列码,它返回一个int类型的值,哈希散列码的作用是确定该对象在哈希表中的索引位置,目的是为了支持Map接口。在Object类中hashCode是一个native方法,它是由在虚拟机堆的位置唯一确定,一般在重写该方法时需要自己定义其中的算法。

重写equals时必须重写hashCode方法?

其实是不一定的,网上很多文章都说必须同时重写,这是建立在设计合理的基础上。如果一个类不涉及HashSet、Hashtable、HashMap等内部使用哈希表的数据结构的类时,可以不必重写hashCode方法,因为如果不涉及哈希表hashCode就毫无意义。但是在实际编码时又要求同时重写,因为你无法预测该类是否会应用到含有哈希表的类,所以通常会有“重写equals时必须重写hashCode方法”的说法。

七、String、StringBuilder、StringBuffer区别

可变性

String类不可变,它每次申请固定长度的char字符数组final char value[],并且不可修改,平时所使用的+号字符串拼接实际上是开辟了多个内存空间,最后结果字符串的堆内存可用,其余的空间全部成为垃圾,读者可阅读我曾经写的一篇文章了解:Java中String对象最容易被忽略的知识

StringBuffer和StringBuilder都是可变型字符串类,它们都继承自AbstractStringBuilder类,其中的字符数组定义是可变的char[] value,在其中每次字符串拼接如果容量充足就在当前堆内存改变,如果不足才开辟新的空间,其中每次扩容是原来容量的2倍+2,源码中是这样实现的:(value.length << 1) + 2,最大容量是Integer.MAX_VALUE - 8,为什么减8呢?因为对象头需要占用一定空间,实际占用大小因虚拟机位数而定。

多线程安全

String和StringBuffer是多线程安全的,String的字符数组是final的,所以它不存在修改也就天然线程安全,而StringBuffer则是通过同步锁实现线程安全的,它的所有方法都是使用的synchronized修饰保证其线程安全性。而StringBuilder则是非线程安全的。

适用条件

当字符串拼接很少时适合String类。当字符串拼接很频繁时,如果仅在单线程操作变量,适合StringBuilder;如果在多线程情况下,使用StringBuffer能更好保证其安全性。在单线程情况下,StringBuilder相比于StringBuffer有15%左右的性能提升。

八、"abc"与new String("abc")

通常创建字符串有两种方法,一种是直接使用双引号创建"abc",一种是new一个String类。两种方法都能创建字符串,但其流程却有所差别,详细内容可阅读这篇文章:Java中String对象最容易被忽略的知识

这两种方法涉及到String的intern方法实现,在jdk6和jdk6以后具体实现有所差别,这里只讲jdk6以后的实现。

九、构造函数、构造代码块、静态代码块

先看一下这三个在代码中的样子

public class Test1 {
    Test1() {
        System.out.println("构造函数");
    }

    {
        System.out.println("构造代码块");
    }

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

    public static void main(String[] args) {
        System.out.println("main函数执行");
        new Test1();
    }
}

上面代码的运行结果是:

静态代码块
main函数执行
构造代码块
构造函数

十、final、finally、finalize区别

十一、Java中的值传递与引用传递

因为这部分知识容易饶,所以我将结合代码描述。

值传递

方法传递对象是基本数据类型,方法得到的是参数值的拷贝,无论该方法对其传递变量做什么样的修改,其原本的值均不会改变,因为方法体操作的是拷贝的数据。

public static void main(String[] args) {
    int a = 1, b = 2;
    change(a, b);
    System.out.println("a = " + a + ",b = " + b);
    // 运行结果是:   a = 1,b = 2
}

static void change(int a, int b) {
    a = 3;
    b = 4;
}

引用传递

引用传递的对象是引用数据类型或数组类型,方法得到的是对象的堆内存地址,方法可以改变堆内存中对象的内容,但是它和值传递有一点很容易弄混淆,我相信看下面的代码就不会混淆了。

static class User{
    String name;
    User(String name){
        this.name = name;
    }
}

public static void main(String[] args) {
    User user1 = new User("北风");
    User user2 = new User("tz");

    swap(user1,user2);
    //  该方法是交换user1和user2的堆内存,结果明显是会失败的,
    //  因为它们两个的栈内存并没有改变,仍然指向的是原来的堆内存
    System.out.println("user1:"+user1.name+"; user2:"+user2.name);
    //  运行结果是:    user1:北风; user2:tz

    change(user1,user2);
    //  该方法是改变user1和user2的name属性,会成功,
    //  因为引用传递能修改堆内存的内容
    System.out.println("user1:"+user1.name+"; user2:"+user2.name);
    //  运行结果是:    user1:AAAAAAA; user2:BBBBBBB
}
//  交换user1和user2地址
static void swap(User user1,User user2) {
    User temp = user1;
    user1 = user2;
    user2 = temp;
}
//  修改user1和user2的内容
static void change(User user1,User user2) {
    user1.name = "AAAAAAA";
    user2.name = "BBBBBBB";
}

十二、获得一个类的实例有哪些方法

微信公众号浏览体验更佳,在这里还有更多优秀文章为你奉上,快来关注吧!

北风IT之路