0%

Java面试大全

一、变量自增

1.1 示例

  • 这种计算跟之前数据结构和算法学过的计算器一致,都是要维护一个栈,保存操作数,判断加减乘除运算符,最后栈中经过计算后只剩下一个数,就是我们需要的运算结果
  • Java也需要维护一个操作数栈,来计算我们的最终结果
1
2
3
4
5
6
7
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i="+i);5
System.out.println("j="+j);2
System.out.println("k="+k);12

1.2 分析

1.2.1 前两步

  1. 赋值操作更新局部变量表,变量操作会先将变量值压入操作数栈
  2. 自增操作会更新变量的局部变量表,但不会影响操作数栈的值,存在操作数就会把变量的值压入操作数栈中
  3. 赋值操作是把操作数栈的最终结果赋值给变量,也就是说i经过前两步后值为1

1.2.2 第三步

  • 后增操作:先赋值j,再自增i

1.2.3 第四步

1.3 小结

  • 总的来说在运算的时候维护一个局部变量表和一个操作数栈,来保存更新我们的数据

  • ++i会先修改i保存到局部变量表中的值,再将自增后的值放入到操作数栈中

  • i++与++i相反,会先将i的值放入到操作数栈中,而后再更新i的局部变量表
  • 赋值操作是在操作数栈计算完之后才执行的

二、JDK/JRE/JVM的区别

  • JDK:Java标准开发包,提供编译、运行Java程序所需的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等
  • JRE,Java运行环境,用于运行Java的字节码文件,JRE中包括了JVM工作所需要的类库,普通用户只需要按照JRE来运行Java程序,而程序开发者必须安装JDK来编译、调试程序
  • JVM:Java虚拟机,是JRE的一部分,它是整个Java实现跨平台的最核心的部分,负责运行字节码文件

三、Java数据类型

基础数据类型

  • 整数类型(byte、short、int、long)
  • 浮点类型(float、double)
  • 数值型
  • 字符型(char)
  • 布尔型(boolean)

引用数据类型

  • 类(class)
  • 接口(interface)
  • 数组([])

四、hashCode()与equals()之间的关系

在了解hashCode()之前,首先要了解哈希表这种数据结构,这里假设存在一个哈希函数f(x),那么它的映射关系是数据->存储地址,即对于一个数据x,它存放的地址为经过函数f(x)得到的地址。

但由于计算机的存储空间不是无限的,所以哈希地址的计算一般会出现重复的情况,这里可以采用链地址法,即如果两条数据计算到的哈希值相同,那么就在这个位置上创建一个链表来存放这两个相同哈希值的数据。

有了哈希表的概念,那么这里Java对象中的hashCode就是经过某一哈希函数得到的哈希值,在应用时参考以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Key {
Integer id;

public Key(Integer id){
this.id = id;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

// @Override
// public int hashCode() {
// return id.hashCode();
// }

// @Override
// public boolean equals(Object obj) {
// if(obj == null || !(obj instanceof Key))
// return false;
// else
// return this.getId().equals(((Key) obj).getId());
// }

public static void main(String[] args) {
Key k1 = new Key(1);
Key k2 = new Key(1);
HashMap<Key,String> hm = new HashMap<Key, String>();

hm.put(k1,"Key with id is 1");
System.out.println(hm.get(k2));
}
}

这段代码测试了使用相同id值的对象在HashMap中进行查找(工作会经常使用到的操作),测试结果如下:

  • 不重写hashCode()和equals()方法,结果输出”null“
  • 重写hashCode()但不重写equals(),结果仍输出“null”
  • 同时重写hashCode()和equals()方法,输出结果”Key with id is 1“,内容可以正常查询到

对以上三种测试情况有以下说明:

  • 集合类HashMap在进行对象查找(get()方法)时,会先根据对象的hashCode进行查找,
    • 如果两个对象的hashCode不同,则说明两个对象绝对是不同的;
    • 如果两个对象的hashCode相同,则调用对象的equals()方法继续进行比较,比较内容相同则两个对象一定是相同的
  • 如果不重写hashCode()和equals()方法,那么对象会采用Object父类实现的默认的hashCode()和equals()方法,默认的hashCode是对象的内存地址,默认equals()的比较方式也是比较两个对象的内存地址是否相同

在使用时一般会遵循以下原则:

  • 重写equals()方法必须重写hashCode()方法
  • 两对象equals()相等,则这两个对象的hashCode()应该相等
  • hashCode()和equals()的返回值应该是稳定的,不应具有随机性
  • 如果要在HashMap的”键“部分存放自定义的对象,一定要在这个对象里重写equals和hashCode方法

在面试时会经常问到比如:

  • 有没有重写过hashCode方法
  • 在使用HashMap时有没有重写hashCode和equals方法,怎么写的

五、==与equals的区别

参考第三题,equals()方法在不重写的情况下,默认比较的是两个对象的内存地址,但我们往往会在类中重写equals()来比较对象之间的内容是否相等equals()只能被对象调用

==是Java中的一种操作符,它有两种比较方式

  • 对于基础数据类型来说,==判断的是两边的值是否相等
  • 对于引用类型来说,==判断的是两边的引用是否相等,也就是判断两个对象是否指向了同一块内存区域

equals()方法的特性

  • 自反性:对于任何非空引用值x来说,x.equals(x)返回true
  • 对称性:对于任何非空引用值x、y来说,如果x.equals(y)为true,则y.equals(x)为true
  • 传递性:对于任何非空引用值x、y、z来说,如果x.equals(y)为true,y.equals(z)为true,那么x.equals(z)也为true
  • 一致性:对于任何非空引用值x、y来说,如果x.equals(y)为true,那么它们要始终相等
  • 非空性:对于任何非空引用值x来说,x.equals(null)必须返回false

六、String中的equals是如何重写的

String是Java中的字符串类,它整个类都是被final修饰的,这意味着String类是不能被任何类继承,任何修改String字符串的方法都是创建了一个新的字符串

equals方法是Object类定义的方法,Object是所有类的父类,当然也包括String,String重写的equals方法如下

  • 首先是引用的判断,引用相等则直接返回true
  • 接着判断对象是否是String实例,不是则直接返回false,否则将对象强转后,比较字符串的长度,长度不等肯定返回false
  • 长度相等的情况下,再逐个字符进行比较,存在不同的字符则返回false

注意JDK1.8以后,new String(“abc”)操作和普通的字符串赋值操作作用是一样的,以下的intern()方法是获取常量池中的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
String s1 = "abc";

String s2 = "a" + new String("bc");
String s3 = new String("abc");

System.out.println(s1.intern()); //abc

// 以下输出均为true
System.out.println(s2.equals("abc"));
System.out.println(s3.equals(s1));
System.out.println(s1.intern().equals(s2));
System.out.println(s3.intern().equals(s1));
}
}

七、String s1 = new String(“abc”)在内存中创建了几个对象

一个或者两个,首先new操作一定会在堆中创建一个对象,如果常量池中不存在“abc”对象,那么就会在常量池中创建一个“abc”,否则就不进行创建

观察下面String构造器的源码可以发现,String对象的hash值和常量池“abc”对象的hash值是一致的

new操作的内存情况如下:

八、String为什么是不可变的、jdk源码中的String如何定义的、为什么这么设计

不可变对象:不可变对象就是一经创建后,其对象的内部状态不能被修改,即:

  • 不可变对象内部属性都是final的
  • 不可变对象的内部属性都是private的
  • 不可变对象不能提供任何可以修改内部状态的方法,setter方法也不行
  • 不可变对象不能被继承和扩展

String类是一种对象,它独立于Java基本数据类型而存在的,可以理解为字符串的集合

String被设计为final的,表示String对象一经创建后,它的值就不能再被修改了,任何对String值进行修改的方法就是重新创建一个字符串

String对象创建后会存在于运行时常量池中,运行时常量池属于方法区的一部分,JDK1.7后把它移到了堆中

九、String/StringBuffer/StringBuilder的区别

String类型是不可变的,属于字符串常量

StringBuffer和StringBuilder都是可修改的字符串对象,区别是StringBuffer是线程安全的,StringBuilder是线程不安全的

十、static关键字是干什么用的

  • 修饰变量,static修饰的变量称为静态变量、也称为类变量,类变量属于类所有,对于不同的类来说,static变量只有一份,static修饰的变量位于方法区;static修饰的变量能够直接通过类名.变量名来进行访问,不用通过实例化类再进行使用
  • 修饰方法,static修饰的方法称为静态方法,静态方法能够直接通过类名.方法名来使用,在静态方法内部不能使用非静态属性和方法
  • static可以修饰代码块,主要分为两种,一种直接定义在类中,使用static{},这种被称为静态代码块,一种是在类中定义静态内部类,使用static class xxx来进行定义

  • static可用用于静态导包,通过使用import static xxx来实现,这种方式一般不推荐使用

  • static可以和单例模式一起使用,通过双重检查锁来实现线程安全的单例模式

十一、final关键字是干什么用的

final 是 Java 中的关键字,它表示的意思是 不可变的,在 Java 中,final 主要用来

  • 修饰类,final 修饰的类不能被继承,不能被继承的意思就是不能使用 extends 来继承被 final 修饰的类。
  • 修饰变量,final 修饰的变量不能被改写,不能被改写的意思有两种,对于基本数据类型来说,final 修饰的变量,其值不能被改变,final 修饰的对象对象的引用不能被改变,但是对象内部的属性可以被修改。final 修饰的变量在某种程度上起到了不可变的效果,所以,可以用来保护只读数据,尤其是在并发编程中,因为明确的不能再为 final 变量进行赋值,有利于减少额外的同步开销
  • 修饰方法,final 修饰的方法不能被重写
  • final 修饰符和 Java 程序性能优化没有必然联系

十二、抽象类和接口的区别是什么

抽象类abstract和接口interface都是Java中的关键字,

抽象类和接口的相同点:

  • 都允许进行方法的定义,而不用具体的方法实现
  • 都允许被继承
  • 广泛的应用于 JDK 和框架的源码中,来实现多态和不同的设计模式。

抽象类和接口的不同点:

  • 抽象级别不同:类、抽象类、接口其实是三种不同的抽象级别,抽象程度依次是 接口 > 抽象类 > 类。在接口中,只允许进行方法的定义,不允许有方法的实现,抽象类中可以进行方法的定义和实现;而类中只允许进行方法的实现
  • 使用的关键字不同:类使用 class 来表示;抽象类使用 abstract class 来表示;接口使用 interface 来表示
  • 变量:接口中定义的变量只能是公共的静态常量,抽象类中的变量是普通变量。

十三、重写和重载的区别

  • 重写是针对子类和父类的表现形式,而重载是在同一类中的不同表现形式
  • 子类重写父类的方法一般使用@override来表示,重写后的方法其方法的声明和参数类型、顺序必须要和父类完全一致
  • 重载是针对同一类中概念,它要求重载的方法必须满足下面任何一个要求:方法参数的顺序、参数的个数、参数的类型任意一个保持不同即可

十四、byte的取值范围

Java的Byte的取值范围为(-128,127),占用一个字节,即8bit

Java中使用补码来表示二进制数,因此最高位为符号位,最高位为0表示正数,最高为为1表示负数

先来复习下源码、反码、补码

原码:正常的二进制计算,存在符号位,0表示正数,最高为为1表示负数

反码:正数的反码与其原码相同;负数在原码基础上,按位取反,符号位除外

补码:正数的补码与其原码相同;负数的补码是在其反码的末位+1

byte中,正数的最大值就是0111 1111

因为存在符号位,所以存在两个0,即-0:1000 0000和+0:0000 0000,因此用1000 0000来表示负数的最小值,即-128

1000 0000减1后为0111 1111,再取反1000 0000,原码的值128,在负数中就应该为-128

十五、HashMap和HashTable的区别

不同点:

  • 父类不同:HashMap继承了AbstractMap类,而HashTable继承了Dictionary类
  • 空值不同:HashMap允许空的key和value值,HashTable不允许空的key和value值。HashMap会把Null key当做普通的key对待,且不允许null key重复。HashTable存null的key就会报空指针异常

  • 线程安全性:HashMap是线程不安全的,如果多个外部操作同时修改HashMap的数据结构比如add或者是delete,必须进行同步操作,即仅仅对key或者value的修改不是改变数据结构的操作

  • 性能方面:虽然HashMap和HashTable都是基于单链表的,但是HashMap进行put或者get操作,可以达到常数时间的性能;而HashTable的put和get操作都是加了synchronized,所以效率很差
  • 初始容量不同:HashTable的初始长度是11,之后每次扩充容量变为之前的2n+1(n为上一次的长度)而HashMap的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小