导图社区 java基础(面试必背)
包含java基础知识,底层描述以及代码举例
编辑于2020-03-15 11:34:51java基础(面试必背)
1. 数据类型
1.1. 基本类型
byte/8 char/16 short/16 int/32 float/32 long/64 double/64 boolean/~ boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
1.2. 包装类
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 Integer x = 2; // 装箱 调用了 Integer.valueOf(2) int y = x; // 拆箱 调用了 X.intValue()
1.3. 缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于: new Integer(123) 每次都会新建一个对象; Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。 valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。 不在的话就new一个新对象 在 Java 8 中,Integer 缓存池的大小默认为 -128~127。 和short一样 编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。 Integer m = 123; Integer n = 123; System.out.println(m == n); // true Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的 通过jvm设置
2. String
2.1. String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
2.2. Java 8 中,String 内部使用 char 数组存储数据。在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。
2.3. (final修饰)不可变的好处
可以缓存hash值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
2.4. String, StringBuffer and StringBuilder
可变性
String 不可变 StringBuffer 和 StringBuilder 可变
线程安全性
String 不可变,因此是线程安全的 StringBuilder 不是线程安全的 StringBuffer 是线程安全的,内部使用 synchronized 进行同步
2.5. String Pool
字符串常量池(String Pool)保存着所有字符串字面量(literal strings)即直接用双引号赋值定义的字符串
intern()方法
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。 下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得同一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。 String s1 = new String("aaa"); String s2 = new String("aaa"); System.out.println(s1 == s2); // false String s3 = s1.intern(); String s4 = s1.intern(); System.out.println(s3 == s4); // true 如果是采用 "bbb" 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。 String s5 = "bbb"; String s6 = "bbb"; System.out.println(s5 == s6); // true 在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。
2.6. new String("abc")
创建了两个对象,都在堆中,一个在Constant Pool中,一个在String Pool中,忽后者作为创建前者的构造参数,两者指向同一个value数组;
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。 "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量; 而使用 new 的方式会在堆中创建一个字符串对象。 以下是 String 构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全复制 value 数组内容,而是都会指向同一个 value 数组。 public String(String original) { this.value = original.value; this.hash = original.hash; }
3. 运算
3.1. 参数传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变传递的对象的字段值会改变原对象该字段值,因为引用的是同一个对象。 但是在方法中将指针引用了其它对象,那么此时方法里和方法外的两个指针指向了不同的对象,在一个指针改变其所指向对象的内容对另一个指针所指向的对象没有影响。 public class PassByValueExample { public static void main(String[] args) { Dog dog = new Dog("A"); System.out.println(dog.getObjectAddress()); // Dog@4554617c func(dog); System.out.println(dog.getObjectAddress()); // Dog@4554617c System.out.println(dog.getName()); // A } private static void func(Dog dog) { System.out.println(dog.getObjectAddress()); // Dog@4554617c dog = new Dog("B"); //新建的对象在堆中,和传递进来的不是同一个 System.out.println(dog.getObjectAddress()); // Dog@74a14482 System.out.println(dog.getName()); // B } }
3.2. float与double
1.1字面量属于double类型 1.1f才是float类型
Java 不能隐式执行向下转型,因为这会使得精度降低。 1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。 // float f = 1.1; 1.1f 字面量才是 float 类型。 float f = 1.1f;
隐式类型转换
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型向下转型为 short 类型。 short s1 = 1; // s1 = s1 + 1; 但是使用 += 或者 ++ 运算符会执行隐式类型转换。 s1 += 1; s1++; 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: s1 = (short) (s1 + 1);
3.3. switch
从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。
switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
4. 关键字
4.1. final
修饰数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 对于基本类型,final 使数值不变; 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 final int x = 1; // x = 2; // cannot assign value to final variable 'x' final A y = new A(); y.a = 1;
修饰方法
声明方法不能被子类重写。 private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
修饰类
声明类不允许被继承。
4.2. static
静态变量
静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 public class A { private int x; // 实例变量 private static int y; // 静态变量 public static void main(String[] args) { // int x = A.x; // Non-static field 'x' cannot be referenced from a static context A a = new A(); int x = a.x; int y = A.y; } }
静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 public abstract class A { public static void func1(){ } // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' } 只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。 public class A { private static int x; private int y; public static void func1(){ int a = x; // int b = y; // Non-static field 'y' cannot be referenced from a static context // int b = this.y; // 'A.this' cannot be referenced from a static context } }
不能是抽象方法(abstract)
不能有this和super关键字
静态语句块
静态语句块在类初始化时运行一次。 public class A { static { System.out.println("123"); } public static void main(String[] args) { A a1 = new A(); A a2 = new A(); } } 123
静态语句块在类初始化时运行一次。
静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。 public class OuterClass { class InnerClass { } static class StaticInnerClass { } public static void main(String[] args) { // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context OuterClass outerClass = new OuterClass(); InnerClass innerClass = outerClass.new InnerClass(); StaticInnerClass staticInnerClass = new StaticInnerClass(); } } 静态内部类不能访问外部类的非静态的变量和方法。
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 import static com.xxx.ClassName.*
初始化顺序
存在继承的情况下,初始化顺序为: 父类(静态变量、静态语句块) 子类(静态变量、静态语句块) 父类(实例变量、普通语句块) 父类(构造函数) 子类(实例变量、普通语句块) 子类(构造函数)
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
5. Object 通用方法
5.1. equals()
对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。 Integer x = new Integer(1); Integer y = new Integer(1); System.out.println(x.equals(y)); // true System.out.println(x == y); // false
底层实现
检查是否为同一个对象的引用,如果是直接返回 true; 检查是否是同一个类型,如果不是,直接返回 false; 将 Object 对象进行转型; 判断每个关键域是否相等。
5.2. hadhcode()
hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。
理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失
5.3. toString()
默认返回 对象名@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
5.4. clone()
浅拷贝:拷贝对象和原始对象的引用类型应用同一个对象
深拷贝:引用不同的对象
尽量不要使用此方法,用拷贝函数或拷贝工厂
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
6. 继承
6.1. 访问权限
ava 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见。
如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。
使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。
6.2. 抽象类与接口
如果一个类中包含抽象方法,那么这个类必须声明为抽象类。
抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。从 Java 8 开始,接口也可以拥有默认的方法实现
接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 接口的字段默认都是 static 和 final 的。
区别
从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。 在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
重写与重载
重写原则
为了满足里式替换原则,重写有以下三个限制: 子类方法的访问权限必须大于等于父类方法; 子类方法的返回类型必须是父类方法返回类型或为其子类型。 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
返回值不同,其它都相同不算是重载。存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
7. 反射
7.1. 反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。
7.2. 优点
可扩展性
7.3. 缺点
性能、安全、内部暴露
8. 异常
8.1. error
编译不通过
8.2. exception
受检异常(可以try catch)和非受检异常
9. 泛型
9.1. 进行使用前不知道具体是什么类型,提供了编译期的类型安全,避免了频繁类型转换的麻烦
9.2. 原理:泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List来表示。
9.3. 限定通配符和非限定通配符
限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
9.4. Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
10. jdk8新特性
10.1. JDK>JRE>JVM,JRE支持了java程序的运行,而JDK则同时支持了java程序的开发。
10.2. Lambda表达式和函数式接口
允许我们将函数当成参数传递给某个方法
10.3. Java 8使用两个新概念扩展了接口的含义:默认方法和静态方法
10.4. 方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。