万字梳理,带你拿下 Java 面试题!
-
并发性的:你可以在其中执行许多语句,而不必一次执行它; -
面向对象的:基于类和面向对象的编程语言; -
独立性的:支持一次编写,到处运行的独立编程语言,即编译后的代码可以在支持 Java 的所有平台上运行。
-
简单,Java 会让你的工作变得更加轻松,使你把关注点放在主要业务逻辑上,而不必关心指针、运算符重载、内存回收等与主要业务无关的功能。 -
便携性,Java 是平台无关性的,这意味着在一个平台上编写的任何应用程序都可以轻松移植到另一个平台上。 -
安全性, 编译后会将所有的代码转换为字节码,人类无法读取。它使开发无病毒,无篡改的系统/应用成为可能。 -
动态性,它具有适应不断变化的环境的能力,它能够支持动态内存分配,从而减少了内存浪费,提高了应用程序的性能。 -
分布式,Java 提供的功能有助于创建分布式应用。使用远程方法调用(RMI),程序可以通过网络调用另一个程序的方法并获取输出。您可以通过从互联网上的任何计算机上调用方法来访问文件。这是革命性的一个特点,对于当今的互联网来说太重要了。 -
健壮性,Java 有强大的内存管理功能,在编译和运行时检查代码,它有助于消除错误。 -
高性能,Java 最黑的科技就是字节码编程,Java 代码编译成的字节码可以轻松转换为本地机器代码。通过 JIT 即时编译器来实现高性能。 -
解释性,Java 被编译成字节码,由 Java 运行时环境解释。 -
多线程性,Java支持多个执行线程(也称为轻量级进程),包括一组同步原语。这使得使用线程编程更加容易,Java 通过管程模型来实现线程安全性。
-
值传递是指在调用函数时将实际参数复制一份到函数中,这样的话如果函数对其传递过来的形式参数进行修改,将不会影响到实际参数 -
引用传递是指在调用函数时将对象的地址直接传递到函数中,如果在对形式参数进行修改,将影响到实际参数的值。
-
对于基本数据类型来说, == 判断的是两边的值是否相等;
public class DoubleCompareAndEquals {
Person person1 = new Person(24,"boy");
Person person2 = new Person(24,"girl");
int c = 10;
private void doubleCompare(){
int a = 10;
int b = 10;
System.out.println(a == b);
System.out.println(a == c);
System.out.println(person1.getId() == person2.getId());
}
}
-
对于引用类型来说, == 判断的是两边的引用是否相等,也就是判断两个对象是否指向了同一块内存区域。
private void equals(){
System.out.println(person1.getName().equals(person2.getName()));
}
-
自反性:对于任何非空引用 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) 相等的话,那么它们必须始终相等。 -
非空性:对于任何非空引用的值 x 来说,x.equals(null) 必须返回 false。
-
首先会判断要比较的两个字符串它们的引用是否相等。如果引用相等的话,直接返回 true ,不相等的话继续下面的判断; -
然后再判断被比较的对象是否是 String 的实例,如果不是的话直接返回 false,如果是的话,再比较两个字符串的长度是否相等,如果长度不想等的话也就没有比较的必要了;长度如果相同,会比较字符串中的每个 字符 是否相等,一旦有一个字符不相等,就会直接返回 false。
if (this == anObject) {
return true;
}
private void StringOverrideEquals(){
String s1 = "aaa";
String s2 = "aa" + new String("a");
String s3 = new String("aaa");
System.out.println(s1.intern().equals(s1));
System.out.println(s1.intern().equals(s2));
System.out.println(s3.intern().equals(s1));
}
-
首先 s1.intern.equals(s1) 这个无论如何都返回 true,因为 s1 字符串创建出来就已经在常量池中存在了。 -
然后第二条语句返回 false,因为 s1 返回的是常量池中的对象,而 s2 返回的是堆中的对象 -
第三条语句 s3.intern.equals(s1),返回 true ,因为 s3 对象虽然在堆中创建了一个对象,但是 s3 中的 "aaa" 返回的是常量池中的对象。
String str1 = "通话";
String str2 = "重地";
-
如果在 Java 运行期间对同一个对象调用 hashCode 方法后,无论调用多少次,都应该返回相同的 hashCode,但是在不同的 Java 程序中,执行 hashCode 方法返回的值可能不一致; -
如果两个对象的 equals 相等,那么 hashCode 必须相同; -
如果两个对象 equals 不相等,那么 hashCode 也有可能相同,所以需要重写 hashCode 方法,因为你不知道 hashCode 的底层构造(反正我是不知道,有大牛可以传授传授),所以你需要重写 hashCode 方法,来为不同的对象生成不同的 hashCode 值,这样能够提高不同对象的访问速度; -
hashCode 通常是将地址转换为整数来实现的。
-
不可变对象的内部属性都是 final 的; -
不可变对象的内部属性都是 private 的; -
不可变对象不能提供任何可以修改内部状态的方法、setter 方法也不行; -
不可变对象不能被继承和扩展。
-
修饰变量,static 修饰的变量称为静态变量、也称为类变量,类变量属于类所有,对于不同的类来说,static 变量只有一份,static 修饰的变量位于方法区中;static 修饰的变量能够直接通过 类名.变量名 来进行访问,不用通过实例化类再进行使用; -
修饰方法,static 修饰的方法被称为静态方法,静态方法能够直接通过 类名.方法名 来使用,在静态方法内部不能使用非静态属性和方法; -
static 可以修饰代码块,主要分为两种,一种直接定义在类中,使用 static{},这种被称为静态代码块,一种是在类中定义静态内部类,使用 static class xxx 来进行定义; -
static 可以用于静态导包,通过使用 import static xxx 来实现,这种方式一般不推荐使用; -
static 可以和单例模式一起使用,通过双重检查锁来实现线程安全的单例模式。
-
修饰类,final 修饰的类不能被继承,不能被继承的意思就是不能使用 extends 来继承被 final 修饰的类; -
修饰变量,final 修饰的变量不能被改写,不能被改写的意思有两种,对于基本数据类型来说,final 修饰的变量,其值不能被改变,final 修饰的对象,对象的引用不能被改变,但是对象内部的属性可以被修改。final 修饰的变量在某种程度上起到了不可变的效果,所以,可以用来保护只读数据,尤其是在并发编程中,因为明确的不能再为 final 变量进行赋值,有利于减少额外的同步开销; -
修饰方法,final 修饰的方法不能被重写; -
final 修饰符和 Java 程序性能优化没有必然联系。
-
抽象级别不同:类、抽象类、接口其实是三种不同的抽象级别,抽象程度依次是 接口 > 抽象类 > 类。在接口中,只允许进行方法的定义,不允许有方法的实现,抽象类中可以进行方法的定义和实现;而类中只允许进行方法的实现,我说的方法的定义是不允许在方法后面出现 {} -
使用的关键字不同:类使用 class 来表示;抽象类使用 abstract class 来表示;接口使用 interface 来表示 -
变量:接口中定义的变量只能是公共的静态常量,抽象类中的变量是普通变量。
-
子父级关系不同,重写是针对子级和父级的不同表现形式,而重载是在同一类中的不同表现形式; -
概念不同,子类重写父类的方法一般使用 @override 来表示;重写后的方法其方法的声明和参数类型、顺序必须要与父类完全一致;重载是针对同一类中概念,它要求重载的方法必须满足下面任何一个要求:方法参数的顺序,参数的个数,参数的类型任意一个保持不同即可。
-
父类不同:HashMap 继承了 AbstractMap 类,而 HashTable 继承了 Dictionary 类:
-
空值不同:HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。
-
线程安全性:HashMap 不是线程安全的,如果多个外部操作同时修改 HashMap 的数据结构比如 add 或者是 delete,必须进行同步操作,仅仅对 key 或者 value 的修改不是改变数据结构的操作。可以选择构造线程安全的 Map 比如 Collections.synchronizedMap或者是 ConcurrentHashMap。而 HashTable 本身就是线程安全的容器。 -
性能方面:虽然 HashMap 和 HashTable 都是基于单链表的,但是 HashMap 进行 put 或者 get 操作,可以达到常数时间的性能;而 HashTable 的 put 和 get 操作都是加了 synchronized 锁的,所以效率很差。 -
初始容量不同:HashTable 的初始长度是11,之后每次扩充容量变为之前的 2n+1(n为上一次的长度)而 HashMap 的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。
do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
Collections.synchronizedMap(new HashMap());
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table 为null 或者没有为table分配内存,就resize一次
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 指定hash值节点为空则直接插入,这个(n - 1) & hash才是表中真正的哈希
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果不为空
else {
Node<K,V> e; K k;
// 计算表中的这个真正的哈希值与要插入的key.hash相比
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 若不同的话,并且当前节点已经在 TreeNode 上了
else if (p instanceof TreeNode)
// 采用红黑树存储方式
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// key.hash 不同并且也不再 TreeNode 上,在链表上找到 p.next==null
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 在表尾插入
p.next = newNode(hash, key, value, null);
// 新增节点后如果节点个数到达阈值,则进入 treeifyBin() 进行再次判断
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果找到了同hash、key的节点,那么直接退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 更新 p 指向下一节点
p = e;
}
}
// map中含有旧值,返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// map调整次数 + 1
++modCount;
// 键值对的数量达到阈值,需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
-
首先会判断 HashMap 中是否是新构建的,如果是的话会首先进行 resize; -
然后判断需要插入的元素在 HashMap 中是否已经存在(说明出现了碰撞情况),如果不存在,直接生成新的k-v 节点存放,再判断是否需要扩容; -
如果要插入的元素已经存在的话,说明发生了冲突,这就会转换成链表或者红黑树来解决冲突,首先判断链表中的 hash,key 是否相等,如果相等的话,就用新值替换旧值,如果节点是属于 TreeNode 类型,会直接在红黑树中进行处理,如果 hash ,key 不相等也不属于 TreeNode 类型,会直接转换为链表处理,进行链表遍历,如果链表的 next 节点是 null,判断是否转换为红黑树,如果不转换的话,在遍历过程中找到 key 完全相等的节点,则用新节点替换老节点。
U+ 0000 ~ U+ 007F: 0XXXXXXX
U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
-
Arrays.asList 转换完成后的 List 不能再进行结构化的修改,什么是结构化的修改?就是不能再进行任何 List 元素的增加或者减少的操作。
public static void main(String[] args) {
Integer[] integer = new Integer[] { 1, 2, 3, 4 };
List integetList = Arrays.asList(integer);
integetList.add(5);
}
Exception in thread "main" java.lang.UnsupportedOperationException
// 这是 java.util.Arrays 的内部类,而不是 java.util.ArrayList
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
-
sort 方法,对当前集合进行排序, 实现 Comparable 接口的类,只能使用一种排序方案,这种方案叫做自然比较; -
比如实现线程安全的容器 Collections.synchronizedList、 Collections.synchronizedMap 等; -
reverse 反转,使用 reverse 方法可以根据元素的自然顺序 对指定列表按降序进行排序; -
fill,使用指定元素替换指定列表中的所有元素。
-
ArrayList 的底层是动态数组,它是基于数组的特性而演变出来的,所以ArrayList 遍历访问非常快,但是增删比较慢,因为会涉及到数组的拷贝。ArrayList 是一个非线程安全的容器,在并发场景下会造成问题,如果想使用线程安全的容器的话,推荐使用 Collections.synchronizedList;ArrayList 在扩容时会增加 50% 的容量。 -
LinkedList 的底层是双向链表,所以 LinkedList 的增加和删除非常快,只需把元素删除,把各自的指针指向新的元素即可。但是 LinkedList 遍历比较慢,因为只有每次访问一个元素才能知道下一个元素的值。LinkedList 也是一个非线程安全的容器,推荐使用 Collections.synchronizedList -
Vector 向量是最早出现的集合容器,Vector 是一个线程安全的容器,它的每个方法都粗暴的加上了 synchronized 锁,所以它的增删、遍历效率都很低。Vector 在扩容时,它的容量会增加一倍。
-
int 是 Java 中的基本数据类型,int 代表的是 整型,一个 int 占 4 字节,也就是 32 位,int 的初始值是默认值是 0 ,int 在 Java 内存模型中被分配在栈中,int 没有方法; -
Integer 是 Java 中的基本数据类型的包装类,Integer 是一个对象,Integer 可以进行方法调用,Integer 的默认值是 null,Integer 在 Java 内存模型中被分配在堆中。int 和 Integer 在计算时可以进行相互转换,int -> Integer 的过程称为 装箱,Integer -> int 的过程称为 拆箱,Integer 还有 IntegerCache ,会自动缓存 -128 - 127 中的值。
-
比如全局唯一性可以用单例模式; -
可以使用策略模式优化过多的 if...else... -
制定标准用模版模式; -
接手其他人的锅,但不想改原来的类用适配器模式; -
使用组合而不是继承; -
使用装饰器可以制作加糖、加奶酪的咖啡; -
代理可以用于任何中间商......
-
Comparable 更像是自然排序 -
Comparator 更像是定制排序
-
hashCode():用于计算对象的哈希码 -
equals():用于对象之间比较值是否相等 -
toString(): 用于把对象转换成为字符串 -
clone(): 用于对象之间的拷贝 -
wait(): 用于实现对象之间的等待 -
notify(): 用于通知对象释放资源 -
notifyAll(): 用于通知所有对象释放资源 -
finalize(): 用于告知垃圾回收器进行垃圾回收 -
getClass(): 用于获得对象类
-
对象实例.getClass(); -
通过 Class.forName() 创建; -
对象实例.newInstance() 方法创建。
-
软可达:软可达就是我们只能通过软引用才能访问的状态,软可达的对象是由SoftReference 引用的对象,并且没有强引用的对象。软引用是用来描述一些还有用但是非必须的对象。垃圾收集器会尽可能长时间的保留软引用的对象,但是会在发生 OutOfMemoryError 之前,回收软引用的对象。如果回收完软引用的对象,内存还是不够分配的话,就会直接抛出 OutOfMemoryError。 -
弱可达:弱可达的对象是 WeakReference 引用的对象。垃圾收集器可以随时收集弱引用的对象,不会尝试保留软引用的对象。 -
幻象可达:幻象可达是由 PhantomReference 引用的对象,幻象可达就是没有强、软、弱引用进行关联,并且已经被 finalize 过了,只有幻象引用指向这个对象的时候。
-
强可达:就是一个对象刚被创建、初始化、使用中的对象都是处于强可达的状态; -
不可达(unreachable):处于不可达的对象就意味着对象可以被清除了。
-
成员内部类 -
局部内部类 -
匿名内部类 -
静态内部类
-
匿名内部类必须继承一个抽象类或者实现一个接口; -
匿名内部类不能定义任何静态成员和静态方法; -
当所在的方法的形参需要被匿名内部类使用时,必须声明为 final; -
匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
-
NullPointerException: 空指针异常 -
NoSuchMethodException:找不到方法 -
IllegalArgumentException:不合法的参数异常 -
IndexOutOfBoundException: 数组下标越界异常 -
IOException:由于文件未找到、未打开或者I/O操作不能进行而引起异常 -
ClassNotFoundException :找不到文件所抛出的异常 -
NumberFormatException:字符的UTF代码数据格式有错引起异常; -
InterruptedException:线程中断抛出的异常
-
编译时期是由编译器将源文件编译成字节码的过程; -
字节码文件由Java虚拟机解释执行。
-
静态绑定 == 前期绑定 == 编译时绑定 -
动态绑定 == 后期绑定 == 运行时绑定
public class Person {
private String talk;
private String canTalk(){
return talk;
}
}
class Animal{
public static void main(String[] args) {
Person p = new Person();
// private 修饰的方法是Person类独有的,所以Animal类无法访问(动物本来就不能说话)
// p.canTalk();
}
}
public class Fruit {
private String fruitName;
final String eatingFruit(String name){
System.out.println("eating " + name);
return fruitName;
}
}
class Apple extends Fruit{
// 不能重写final方法,eatingFruit方法只属于Fruit类,Apple类无法调用
// String eatingFruit(String name){
// super.eatingFruit(name);
// }
String eatingApple(String name){
return super.eatingFruit(name);
}
}
public class SuperClass {
public static void sayHello(){
System.out.println("由 superClass 说你好");
}
}
public class SubClass extends SuperClass{
public static void sayHello(){
System.out.println("由 SubClass 说你好");
}
public static void main(String[] args) {
SuperClass.sayHello();
SubClass.sayHello();
}
}
public class Father {
void drinkMilk(){
System.out.println("父亲喜欢喝牛奶");
}
}
public class Son extends Father{
@Override
void drinkMilk() {
System.out.println("儿子喜欢喝牛奶");
}
public static void main(String[] args) {
Father son = new Son();
son.drinkMilk();
}
}
-
继承 -
重写 -
父类对象指向子类引用
-
虚拟机提取对象的实际类型的方法表; -
虚拟机搜索方法签名; -
调用方法。
-
编译期触发,能够提早知道代码错误; -
提高程序运行效率。
使用动态绑定的前提条件能够提高代码的可用性,使代码更加灵活;
多态是设计模式的基础,能够降低耦合性。
【End】
CSDN 618程序员购物日:显示器、键盘、蓝牙耳机、扫地机器人、任天堂游戏机、AirPods Pro等超多IT人的心仪好物,全场超低价出售,让1亿程序员买到爽!
更多精彩推荐
☞
☞
你点的每个“在看”,我都认真当成了喜欢