Java八股文
基本数据类型
8种基本数据类型及包装类
- byte(8位、1字节)Byte
- char(16位、2字节)Character
- short(16位、2字节)Short
- int(32位、4字节)Integer
- long(64位、8字节)Long
- float(32位、4字节)Float
- double(64位、8字节)Double
- boolean(true、false)Boolean
包装类的装箱与拆箱
1 | // 装箱 Integer a = Integer.valueOf(10); |
缓存池
Integer装箱源码,如果缓存池已存在数值,那么就直接返回该对象,不用创建一个新对象。
1 | public static Integer valueOf(int i) { |
所以以下结果是true,j是从缓存池中取的相同对象
1 | Integer i = 1; |
缓存池是有范围的,如Integer是-128~127,范围外创建就是两个对象,不会从常量池拿取。注意float和double型是没有缓存池的。
引用数据类型的浅拷贝与深拷贝
今天做唯品会笔试时,发现set复制出了问题,兜兜转转找到了问题的根源,引用数据类型以及深浅拷贝的问题。于是便补上了这一块的笔记。
首先Java分为基础数据类型和引用数据类型,这个我们都知道。对于八大基础数据类型,没有深浅拷贝的说法。我们主要是讨论引用数据类型。
浅拷贝
简单来说,浅拷贝就是将两个引用指向同一个对象。也就是使用 “=” 进行引用传递,引用数据类型存储的是一指向对象的地址。
我们这里set和temp就是使用 “=” 进行浅拷贝,由temp对集合进行清空操作,set和temp指向的是同一对象,所以二者都为空。
1
2
3
4
5
6
7Set<Character> set = new HashSet<>();
Set<Character> temp = new HashSet<>();
temp = set;
System.out.println(temp == set); //true
temp.clear();
System.out.println(temp.isEmpty()); //true
System.out.println(set.isEmpty()); //true深拷贝
和浅拷贝相反,深拷贝就是创建一个新的对象,将原内容进行复制,仅仅是内容相同,但是是两个对象。对于容器来说可以使用 addAll() 或 putAll() 来进行深拷贝。
这里set和temp是两个不同的对象,只是内容一致,也就是说这两个引用指向了两个不同的地址,但地址的内容一样。我们通常使用深拷贝对容器进行复制操作。
1
2
3
4
5
6
7
8Set<Character> set = new HashSet<>();
Set<Character> temp = new HashSet<>();
set.add('s');
temp.addAll(set);
System.out.println(temp == set); //false
temp.clear();
System.out.println(temp.isEmpty()); //true
System.out.println(set.isEmpty()); //false
==与equals的区别
==
- 基本数据类型:比较值是否相等
- 引用类型:比较变量是否引用同一个对象,也就是比较引用变量的值,因为变量存储的是对象的地址
equals()
- 基本数据类型:没有equals()方法
- 引用类型:根据当前类是否重写equals()方法
- 类没有重写equals()方法:此时使用equals()相当于使用 == ,比较两个变量引用的对象地址,此时使用的equals()是Object类的方法
- 类重写了equals()方法:此时equasl()按其重写方法的具体内容执行操作,一般重写都是为了比较引用对象的值是否相等,Integer、String等类都对equals()进行过重写,所以它们的equals()可比较值相等
a、b是直接创建一个abc的新对象,==不等、equals相等。
aa创建一个新对象,且放到了字符串的常量池(类似包装类的缓存池),所以bb创建时直接从常量池拿对象,也就是aa和bb是完全相同的对象,==、equals都相等。
1 | String a = new String("abc"); |
hashCode()与equals()
hashCode()即获得散列值,其定义在Object类中,这代表Java中所有类都包含hashCode()。两个对象等价,其hashcode一定相等,而hashcode相等的两个对象不一定等价,这是因为散列值会发生哈希冲突,使得两个不同的对象有相同的散列值。
而我们重写equals()方法,其hashCode()也要重写,因为两个对象等价,其散列值一定相等。例如我们使用HashSet存储对象时,会先通过散列值判断,若没有相同的散列值则加入对象,有相等的值则用equals再次判断,确定是否是相同的对象。
所以HashSet是先通过hashcode判断对象,再用equals判断对象,如果我们重写equals()时不重写hashCode(),则可能将两个相同的对象存放到HashSet中。而在HashSet存储中使用hashCode()还可以减少equals的判断次数,提高了执行效率,为此我们重写equals()时hashCode()也要重写,为了保证相同的对象有相同的散列值。
泛型擦除
Java只在编译阶段检测非法类型,实际执行时不检测,也就是在编译阶段就将泛型的类型擦除了。如下面的例子,list是存储Integer型的,test被list赋值也应该存储Integer的,但test添加字符串却没有问题,因为他虽然是存储Integer,但在运行时,list和test都会进行泛型擦除,视为ArrayList。可以说泛型只是表面进行限制,实际执行的时候没有用。
list添加字符串是一定不行的,因为表面上它就是用来存储Integer型的,但test虽然等于list,逻辑上是用来存储Integer的,但看上去它没有受泛型限制,所以它可以添加字符串。泛型只是在编译阶段进行限制,也就是表面限制,在实际执行阶段list、test都会被视为ArrayList。
1 | List<Integer> list = new ArrayList<>(); |
重载与重写
重载
重载发生在同一个类中,多个方法名相同的方法,但参数数量、参数顺序、参数类型至少有一个不同。多用于构造器重载。
注意方法返回值类型可以不同,但它不是判断重载的标准,也就是说参数没有改变时,只有返回值类型不同则不是重载。
由于子类继承父类,其父类方法在子类中全部实现,此时我们在对方法重载相当于在子类中重载。也就是同一个类,并没有子类对父类重载。
重写
重写是子类对父类进行操作,方法名、参数必须完全相同,访问权限>=父类,返回值类型和抛出异常可以是父类返回类型或其子类型。
父类是private/final/static修饰,其子类不能重写。子类同名方法相当于定义了一个新方法。
构造方法也不能被子类重写。
String
String的特点
String被声明为final,String对象不可变、不可继承。
1 | private final char value[]; |
String不可变性,使其天生具有线程安全性。
String、StringBuffer、StringBuilder
- String不可变,线程安全,每次发生改变都会新生成一个对象。操作少量数据可用。
- StringBuffer可变,其内部方法加了同步锁,是线程安全的,改变对象是对该对象本身操作。多线程操作字符串可用。
- StringBuilder可变,不是线程安全。单线程操作字符串可用。
字符串常量池(String Pool)
1 | //a != b,这是两个对象 |
这里new String是直接创建一个新对象,即a、b是两个不同对象,但值相等。
使用intern()方法会将对应字符串的值存入常量池,然后当常量池存在值相等的字符串时,直接返回常量池中的引用,所以c、d都是aaa字符串在常量池中的引用,所以是相同对象。
e、f就不用说了,会直接引用常量池,没有则新建对象,并放入常量池。
关键字
final
- final修饰的类不可继承,final类中所有成员方法被隐式指定为final方法
- final修饰方法不可重写
- final修饰的变量是常量
- 基础数据类型,数值在初始化后不可更改
- 引用类型,初始化后不可指向另一个对象
static
静态变量(类变量),静态方法
被static修饰的成员属于类,不单属于类的某个对象,被类中所有对象共享。可通过类名直接调用。
静态代码块:在类初始化时运行一次。该类无论创建多少次,静态代码块只执行一次。
静态内部类
静态导包:import static,可导入某个类的指定静态资源。
this
用于引用类的当前实例,指向本类对象。
super
子类访问父类的变量和方法,指向父类对象。
方法初始化顺序
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- main方法
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
父 > 子,静态最优先
这里看一个特殊例子,我们声明一个全局静态对象,它在调用构造方法时会先执行普通方法块,但静态变量以及静态方法块相互不影响,只有按序走到对应静态变量或静态方法块时,这些静态才会执行,且只执行一次。
1 | public class A { |
面向对象
面向对象与面向过程
面向过程:过程最重要,需要顺序执行按部就班,当性能是主要考量因素时使用,一般都是编译成机械码后运行,所以效率高。主要是后期不好维护,难以重复利用。
面向对象:有封装、继承、多态的特性,易维护、易复用、易扩展,能降低耦合性,但性能相对于面向过程可能低一些。
成员变量与局部变量
- 成员变量(类):可以被public、private、static、final等修饰,没有赋初值会自动以默认值赋值。
- 局部变量(方法、代码块):一般不可被修饰符修饰,但final可以。没有赋初值会报错,局部变量不会以默认值自动赋值。
面向对象三大特征
封装
实体类的封装,例如项目中封装数据库表的字段成一个对象,然后对它进行赋值、取值操作。
继承
子类继承父类,提高代码的复用性,子类可以包括父类所有属性和方法(包括私有),但私有属性和方法是不能访问的,只是拥有。
多态(向上转型)
简单说一个对象有多种状态。
1
2
3//列表可以是数组或链表两种实现
List list = new ArrayList();
List list = new LinkedList();要有继承或实现的关系,子类对父类方法进行了重写,以实现多态。
反射(框架中大量使用反射机制)
通过forName、.class等方式获取类,然后再通过操作Field、Method、Constructor对类的元素、方法等进行操作。
异常(Throwable)
Throwable类是所有异常的共同祖先,其有两个重要子类Exception(异常)和Error(错误)。其中异常可以被try-catch捕获,错误无法处理。
Exception:程序可处理的异常,通过catch进行捕获
check可查异常:在源码中必须显性进行捕获处理,会在编译器进行检查。
例:ClassNotFoundException、SQLException等
uncheck不可查异常:运行时异常(RuntimeException),通常是代码的逻辑错误,根据需求进行捕获,并不在编译期强制要求。
例:NullPointerException、ArrayIndexOutOfBoundsException等
Error:程序无法处理的错误,如JVM运行错误、JVM内存不够等错误,该异常发生时,JVM会将线程终止。
I/O流
序列化(serialization)
- 序列化:将Java对象转换为二进制字节流的过程。
- 反序列化:将序列化生成的二进制字节流转换成Java对象的过程。
在Java中序列化,是对class对象进行操作,而在C++中,结构体定义的数据结构和class对应的对象都可以序列化。
transient
使用transient关键字,阻止被修饰的变量进行序列化,对象被反序列化时,被修饰的变量不会被持久化和恢复。
- transient只能修饰变量,不能修饰方法、类。
- transient修饰的变量,反序列化后其变量值置为类型默认值。
- static变量不会序列化。因为static不属于任何对象,只属于类本身。
键盘输入
Scanner
1
2
3Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();BufferedReader(更快)
1
2BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
IO流
- 输入流、输出流
- 字节流、字符流(有字符流更加方便我们对一些文件字符的操作,并非使用字节处理是最好的,分情况)
- 节点流、处理流