面试Java必定会问到SE部分的基础知识,我也被问过很多次,这篇文章记录一些常问的问题和答案。
所有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)方法和构造方法无法被重写,但是可以被重载,一个类可以有多个被重载的构造方法。
==:如果比较的是基本数据类型,判断其值是否相等;如果比较的是引用类型,判断两个对象的地址是否相等。
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()一起使用。
equals方法上面有介绍,hashCode()定义于Object类中,该方法用于获取哈希散列码,它返回一个int类型的值,哈希散列码的作用是确定该对象在哈希表中的索引位置,目的是为了支持Map接口。在Object类中hashCode是一个native方法,它是由在虚拟机堆的位置唯一确定,一般在重写该方法时需要自己定义其中的算法。
重写equals时必须重写hashCode方法?
其实是不一定的,网上很多文章都说必须同时重写,这是建立在设计合理的基础上。如果一个类不涉及HashSet、Hashtable、HashMap等内部使用哈希表的数据结构的类时,可以不必重写hashCode方法,因为如果不涉及哈希表hashCode就毫无意义。但是在实际编码时又要求同时重写,因为你无法预测该类是否会应用到含有哈希表的类,所以通常会有“重写equals时必须重写hashCode方法”的说法。
可变性
String类不可变,它每次申请固定长度的char字符数组final char value[]
,并且不可修改,平时所使用的+号字符串拼接实际上是开辟了多个内存空间,最后结果字符串的堆内存可用,其余的空间全部成为垃圾,读者可阅读我曾经写的一篇文章了解:Java中String对象最容易被忽略的知识(http://blog.beifengtz.com/article/11,或阅读今日推送的另一篇文章)。
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类。两种方法都能创建字符串,但其流程却有所差别,详细内容可阅读这篇文章:Java中String对象最容易被忽略的知识(http://blog.beifengtz.com/article/11,或阅读今日推送的另一篇文章)。
这两种方法涉及到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函数执行
构造代码块
构造函数
try-finally
或者try-catch-finally
来进行类似关闭 JDBC连接、保证unlock锁等动作。因为这部分知识容易饶,所以我将结合代码描述。
值传递
方法传递对象是基本数据类型,方法得到的是参数值的拷贝,无论该方法对其传递变量做什么样的修改,其原本的值均不会改变,因为方法体操作的是拷贝的数据。
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";
}
END