Java面试总结

1、面向对象的特征

  • 抽象
    将一类对象的共同特征总结出来构造类的过程。
    • 数据抽象
    • 行为抽象
  • 继承
    从已有类得到继承信息创建新类的过程。
  • 封装
    把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
  • 多态
    允许不同子类型的对象对同一消息作出不同的响应。
    • 编译时多态——方法重载(前绑定)
    • 运行时多态——方法重写(后绑定)

2、访问修饰符
外部类 ——public或default
类成员(含内部类)——均可

3、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
前者需要显式强制类型转换,后者隐含类型转换。

4、equal与==
equals方法最初是在所有类的基类Object中进行定义的,源码是

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

可以看出这里定义的equals与==是等效的,但怎么还会不一样呢?
原因就是String类对equals进行了重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}

对equals重写需要注意5点:

1 自反性:对任意引用值X,x.equals(x)的返回值一定为true.
2 对称性:对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3 传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true
4 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
5 非空性:任何非空的引用值X,x.equals(null)的返回值一定为false

经过重写后就跟==有本质的区别了:
equal:比较两个对象内部的内容是否相等的
一般会对equal()方法重写,否则因为所有的类都是Object的子类,而Object类的equal()等效于==。

==:是用来判断两个对象的地址是否相同,即是否是指相同一个对象。
比较的是真正意义上的指针操作。

实现高质量的equals方法的诀窍包括
1 使用==操作符检查”参数是否为这个对象的引用”;
2 使用instanceof操作符检查”参数是否为正确的类型”;
3 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
4 编写完equals方法后,问自己它是否满足对称性、传递性、一致性;
5 重写equals时总是要重写hashCode;
6 不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。

5、int和Integer区别

1
2
3
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
System.out.println(a == b); // false 两个引用没有引用同一对象

new操作另起炉灶

1
2
3
4
5
6
7
public class Test03 {
public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}

整型字面量的值在-128到127之间,不会new新的Integer对象,直接引用常量池中的Integer对象
所以,f1==f2的结果是true,而f3==f4的结果是false。

6、解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。

  • [ ] 存放内容

    堆——new关键字和构造器创建的对象——堆空间;
    栈——基本数据类型的变量,对象引用,函数调用的现场保存;
    方法区——已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;

  • [ ] 堆

    堆是垃圾收集器管理的主要区域,可以细分为新生代和老生代;
    堆和常量池空间不足——OutOfMemoryError

  • [ ] 栈

    栈空间操作最快但是栈很小,通常大量的对象都是放在堆空间。
    栈和堆的大小都可以通过JVM的启动参数来进行调整。
    栈空间不足——StackOverflowError。

7、String类

1
String str = new String("hello");

变量str放在栈上
用new创建出来的字符串对象放在堆上
“hello”字面量是放在方法区的。

  • 补充1
  • 从Java6的某个更新开始,由于JIT编译器的发展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象不一定分配在堆上。
  • 补充2
  • 运行时常量池相当于Class文件常量池,具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,如String类的intern()方法。

看看下面代码的执行结果是什么并且比较一下Java 7以前和以后的运行结果是否一致。

1
2
3
4
5
String s1 = new StringBuilder("go").append("od").toString();
System.out.println(s1.intern() == s1);

String s2 = new StringBuilder("ja").append("va").toString();
System.out.println(s2.intern() == s2);

调用intern()方法之后把字符串对象加入常量池中
JDK8下运行结果为

1
2
true
false

8、switch语句
JDK5之前 byte、short、char、int
JDK5 加入enum
JDK7 加入String
不能使用long等其他类型

9、用最有效率的方法计算2乘以8
答: 2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。

  • 补充
  • 为编写的类重写hashCode方法时,为什么通常选择31这个数?
  • 选择31是因为可以用移位和减法运算来代替乘法,从而得到更好的性能。31 * num 等价于(num << 5) -num,左移5位相当于乘以2的5次方再减去自身就相当于乘以31。

10、构造器(Constructor)是否可被重写(Override)
构造器不能被继承,因此不能被重写,但可以被重载。
Constructor是在一个类创建的时候,用户创建类实例的时候被调用的特殊函数.
它不是一个类的成员函数,它只能代表这个类本身。

11、两个对象值相同(x.equals(y) == true),但却可有不同的hashcode,这句话对不对
不对
equal()遵循以下原则:

  • 对象相同,哈希值必同;
  • 哈希值相同,对象不必相同。

12、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递
Java语言只支持参数的值传递。
当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。

13、String和StringBuilder、StringBuffer的区别
String:不可变,线程安全
StringBuilder:可变,高效(没有synchronized)
StringBuffer:可变,线程安全

示例

1
2
3
4
5
6
7
8
9
10
11
String s1="Programming";
String s2=new String("Programming");//字面量在常量池,new对象在堆区,s2在栈上
String s3="Program";
String s4="ming";
String s5="Program"+"ming";
String s6=s3+s4;
System.out.println(s1==s2);
System.out.println(s1==s5);
System.out.println(s1==s6);
System.out.println(s1==s6.intern());
System.out.println(s2==s2.intern());

JDK8下运行结果

1
2
3
4
5
false //字面量在方法区,new对象在堆区,不是同一个对象
ture //字面量都在方法区的常量池中,且字面相同
false //String对象引用
ture //intern方法会引用或添加其到常量池
false //前者在堆区,后者在常量值

  • String对象的intern()方法
    常量池中存在字面量相同的字符串时,得到字符串对象在常量池中对应的版本的引用;
    不存在对应的字符串时,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;
  • 字符串的+操作
    本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象
    可以用javap -c StringEqualTest.class命令获得class文件对应的JVM字节码指令就可以看出来。

14、重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分
都是多态的实现方式
重载——编译时多态,同一个类中同名方法的不同参数列表形式(参数类型、数量、顺序)
重写——运行时多态,父子类间同名方法的不同参数列表类型(方法名、参数类型相同;返回类型、异常小于父类;访问权限不小于父类)

若允许根据返回类型重载,则没有使用返回值时难以推断,如int i= test();和test()

15、描述JVM加载class文件的原理机制

  • 类的装载是由类加载器(ClassLoader)和它的子类来实现的
  • 类加载器是一个Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
  • 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。
  • 当Java程序需要使用某个类时,JVM会确保这个类已经被加载、链接(验证、准备和解析)和初始化。
  • 类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。
  • 当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。
  • 最后,JVM对类进行初始化,包括:1)如果父类还没有被初始化,先初始化父类;2)中存在初始化语句,依次执行初始化语句。

类加载器包括:

  • 根加载器(BootStrap):一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
  • 扩展加载器(Extension):从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap
  • 系统加载器(System):又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,是用户自定义加载器的默认父加载器。
  • 用户自定义类加载器(java.lang.ClassLoader的子类)。

从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。
PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。

16、char 型变量中能不能存贮一个中文汉字
可以,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。

补充:字节流和字符流
使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于C程序员来说,要完成这样的编码转换恐怕要依赖于union(联合体/共用体)共享内存的特征来实现了。

17、抽象类和接口有什么异同

  • 都不能实例化,都可以定义引用。
  • 必须实现其中所有的方法,否则为抽象类。
  • 抽象类中的方法不一定是抽象的,而接口中必须是;
  • 抽象类成员访问控制符可以任意,而接口只能是public
  • 抽象类可以有构造器,而接口不能;
  • 抽象类可以定义成员变量,而接口只能是常量;

18、静态嵌套类和其他内部类的不同
静态内部类不依赖于外部类实例被实例化,而通常的内部类需要在外部类实例化后才能实例化。主要的区别是没有对外部类引用的特权。

19、Java 中会存在内存泄漏,请简单描述
理论上因为垃圾回收机制不存在,但是在实际开发中可能存在无用且可达的内存对象。极端情况下可能引起物理内存与虚拟内存交换,甚至OutOfMemoryError。

20、抽象的方法是否可同时是静态的,是否可同时是本地方法,是否可同时被synchronized修饰
都不能。
抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

21、阐述静态变量和实例变量的区别

  • 静态变量是被static修饰符修饰的变量,也称为类变量。它属于类,不属于类的任何一个对象,即一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;静态变量可以实现让多个对象共享内存。
  • 实例变量必须依存于类的某一实例,需要先创建对象,然后才能通过对象访问到它。

补充:在Java开发中,上下文类和工具类中通常会有大量的静态成员。

22、是否可以从一个静态方法内部发出对非静态方法的调用
不可以
静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时对象并不一定被初始化了。

23、如何实现对象克隆

  • 实现Cloneable接口,并重写Object类中的clone()方法;
  • 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    public class MyUtil {
    private MyUtil() {
    throw new AssertionError();
    }
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj) throws Exception {
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bout);
    oos.writeObject(obj);
    ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bin);
    return (T) ois.readObject();
    // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
    // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
    }
    }

注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

24、垃圾回收相关参数
与垃圾回收相关的JVM参数:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率

25、String s = new String(“xyz”);创建了几个字符串对象
两个对象
一个是静态区的”xyz”,一个是用new创建在堆上的对象

26、一个Java源文件中是否可以包含多个类(不是内部类),有什么限制
可以
一个源文件中最多只能有一个public,并且文件名必须和public类的类名完全保持一致

27、内部类可以引用它的包含类(外部类)的成员吗,有没有什么限制
除静态内部类外,一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

*28、数据类型之间的转换:

  • 如何将字符串转换为基本数据类型
  • 如何将基本数据类型转换为字符串*

  • 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型;

  • 一种方法是将基本数据类型与空字符串(””)连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf()方法返回相应字符串

29、如何实现字符串的反转及替换
方法很多,可以自定义实现,也可以使用String、StringBuffer或StringBuilder中的方法。
有一道很常见的面试题是用递归实现字符串反转,代码如下所示:

1
2
3
4
5
6
public static String reverse(String originStr) {
if(originStr == null || originStr.length() <= 1)
return originStr;
return reverse(originStr.substring(1)) + originStr.charAt(0);
}
//将字符串的首字母连接到末尾

30、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串

1
2
String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

*31、日期和时间:

  • 如何获取当前准确时间?
  • 如何获取Unix时间戳?
  • 如何获取某月的最后一天?
  • 如何格式化日期?*

  • 当前准确时间

    • 创建java.util.Calendar 实例,调用其get()方法传入不同的参数即可获得参数所对应的值。
    • Java 8中可以使用java.time.LocalDateTime来获取,代码如下所示。
      1
      2
      3
      4
      5
      6
      7
      //Java 8以前
      Calendar cal = Calendar.getInstance();
      System.out.println(cal.get(Calendar.YEAR));

      // Java 8
      LocalDateTime dt = LocalDateTime.now();
      System.out.println(dt.getYear());
  • Unix时间戳

    1
    2
    3
    4
    5
    6
    //Java 8以前
    Calendar.getInstance().getTimeInMillis();
    System.currentTimeMillis();

    //Java 8
    Clock.systemDefaultZone().millis();
  • 月末日期

    1
    2
    Calendar time = Calendar.getInstance();
    time.getActualMaximum(Calendar.DAY_OF_MONTH);
  • 日期格式化

    • java.text.DataFormat的子类(如SimpleDateFormat类)中的format(Date)方法可将日期格式化。
    • Java 8中可以用java.time.format.DateTimeFormatter来格式化时间日期。
1
2
3
4
5
6
7
8
9
//Java 8以前
SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
Date date1 = new Date();
System.out.println(oldFormatter.format(date1));

// Java 8
DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date2 = LocalDate.now();
System.out.println(date2.format(newFormatter));

补充:
Java 8中引入了新的时间日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等类,这些的类的设计都使用了不变模式,是线程安全的。

参考资料
Java面试题全集(上)
Java String类中的intern()方法
关于Java并发编程的总结和思考