八股文 -- Java基础和集合框架
- 1. Java 基础
- JVM, JRE, JDK的关系是什么
- Switch(expr) 中的expr可以用什么类型
- 访问修饰符public, private, protected和default的区别
- final, finally, finalize的区别
- 讲一下static关键字
- 描述一下 Java OOP
- Overload和Override的区别是什么
- 构造方法能不能被重写
- 说一下抽象类
- 说一下接口的作用
- 抽象类可以用final修饰吗, 可以被实例化吗
- 说一说String类
- 如何对比两个String
- --- String为什么要设计成不可变的
- String, StringBuilder和StringBuffer的区别
- 什么是字符串常量池
- Java中的基本类型包括哪些
- 比较包装类怎么比较
- Integer和Int的区别
- 包装类是什么? 基本类型和包装类型有什么区别
- 什么是反射
- 为什么需要泛型, 介绍下泛型
- Java语言是如何处理泛型的 (介绍下类型擦除)
- 什么是泛型中的限定通配符
- 讲一讲Error和Exception
- throw和throws的区别是什么
- Java常见异常有哪些
- try-catch-finally 中, 如果catch中有return, finally还会执行吗
- Java变量命名规范
- i++ 和 ++i的区别
- Object类有哪些方法
- 什么是hashCode以及为什么需要hashCode
- hashCode和equals的关系 / 为什么重写equals一定要重写hashCode方法
- 如何判断double的0值
- --- double和float是如何在内存中储存的
- 2. Java 集合框架
1. Java 基础
JVM, JRE, JDK的关系是什么
- JVM是虚拟机, 负责运行Java程序
- JRE是Java运行环境, 包括Java虚拟机, Java类库
- JDK是Java Development Kit 包括JRE和编译器和其他工具, 比如Java Doc
Switch(expr) 中的expr可以用什么类型
- 可以是 byte, short, char, int, string, enum
访问修饰符public, private, protected和default的区别
- 访问修饰符可以修饰方法, 成员变量. 默认是default
- public 是对所有类可见
- protected是只对一个包内的本类和子类可见
- private是只对本类可见
- default是对同一包的所有类可见
final, finally, finalize的区别
- final
- 修饰变量表示变量值不能被改变
- 修饰方法表示方法不能被重写但是可以被继承
- 修饰类表示类不能被继承
- finally
- fianlly是异常处理中用的关键字
- 表示不管是否发生异常, finally里的语句一定会被执行
- 即使在finally之前有return 比如在try中或者catch中, finally里的代码还是会被执行
- 一般会把资源释放的代码放在finally里, 比如关闭文件等
- finalize: 是Object的自带方法, 和垃圾回收有关
讲一下static关键字
- static关键字可以修饰 变量, 方法, 常量, 类
静态成员变量 和 静态成员常量
- 一个类的所有实例共享静态成员变量, 直接用类名调用
- 即使没有对象实例, 静态成员也存在
//Example: 给每个雇员产生unique ID
public void setId() {
id = nextId;
nextId++;
}
harry.id = Employee.nextId;
Employee.nextId++;
静态成员方法
- 静态方法跟类的对象没有关系, 不管有没有对象实例, 静态方法都存在. 也是直接用类名调用
- 静态方法不能使用非静态成员变量, 但是可以使用静态成员变量
什么时候使用静态方法:
- 当一个方法不需要使用对象的状态, 比如成员变量和成员方法, 也就是所需的参数都是外部直接传入的, 比如:
Math.pow
- 当一个方法使用的都是静态成员变量时
静态类
- 只有内部类可以是静态的, 也只有内部类可以用static关键字修饰
- 静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似
- 它不能使用外部类的非static成员变量或者方法
- 因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象
描述一下 Java OOP
封装
- 封装是将一个概念或者物体封装成一个类. 在类中通过成员变量和成员方法描述这个概念或者物体
比如一个Car类用来描述车这个物体, 那么成员变量可能包括车轮, 排量等描述一个车的物理参数. 成员变量可以包括drive, stop等用来描述车具体的行为.
继承
- 继承的作用是提高代码的复用性, 去除冗余代码, 避免复制代码, 提升代码的拓展性. 在设计代码时, 我们可以将多个类中重复出现的成员变量和方法封装成父类, 这样子类可以通过继承的方式具有父类所有的特性, 并且可以增添只属于子类的方法和变量. 比如之前说的car这个类, 我们可以将所有车共有的基本参数比如车门, 方向盘以及共有的行为比如drive, stop设计成父类. 然后不同牌子的车可以继承car这个父类, 并加入不同品牌特有的feature等.
多态
- 个人觉得Java中多态是基于继承的概念. 然后Java中多态是通过两个方面体现的:
- override和overload: override就是子类拥有父类的方法, 并且可以对父类的方法根据子类的需要进行重写. override必须保持方法的名字, 参数列表和返回值不变, 具体实现可以变. overload是同一个类中可以有方法名一样的方法, 方法的参数和返回值可以不变. Java通过方法的参数列表判断调用哪个方法. 所以每个重载的方法都必须有一个独一无二的参数类型列表. 最常见的是构造方法overload.
- 向上转型和向下转型: 在Java中, 声明一个变量的时候, 等号左边的是变量的声明类型, new 后面的是变量的实际类型. 声明类型是实际类型的父类, 所以在Java中一个变量既可以是子类的类型, 也可以是父类的类型. 最常见的是在写方法时, 参数列表可以使用父类的类型, 这样可以对所有子类使用. 在参数传递时编译器会自动进行向上转型. 然后在方法的实现中可以通过instantof关键字判断变量的实际类型, 并使用强转进行向下转型, 然后进行不同的操作. 这样可以节省代码量, 去除冗余代码.
Overload和Override的区别是什么
- override和overload: override就是子类拥有父类的方法, 并且可以对父类的方法根据子类的需要进行重写. override必须保持方法的名字, 参数列表和返回值不变, 具体实现可以变.
- overload是同一个类中可以有方法名一样的方法, 方法的参数和返回值可以不变. Java通过方法的参数列表判断调用哪个方法. 所以每个重载的方法都必须有一个独一无二的参数类型列表. 最常见的是构造方法overload.
构造方法能不能被重写
- 构造方法不能被继承, 所以不能被重写
说一下抽象类
- 用abstract修饰的类为抽象类, 一般抽象类都包括至少一个或多个抽象方法. 也存在没有抽象方法的抽象类, 用abstract修饰是为了表明这是子类的基类.
- 其中抽象方法只有方法声明没有具体实现,具体是现实是由子类提供. 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法. 抽象类不能被实例化,即不能通过new生成一个抽象类的对象).
- 抽象类不能被实例化,即不能通过new生成一个抽象类的对象, 作用是为某类定义通用方法. (比如一个算几何图形面积和周长的类可以定义两个抽象方法计算面积和周长, 而具体实现通过不同图形子类定义)
- 构造方法必须声明为Protected(因为要供子类使用)
- 子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类)
说一下接口的作用
- 因为java中没有多继承(一个子类只能有一个父类),所以提出了接口的概念
- Java中接口可以多继承, 也就是一个接口同时继承多个接口
- 接口可以使程序的耦合度降低, 发挥多态的优点. 比如我们现在有一个叫Car的基类, 底下有分为电车和油车抽象类, 然后各自有不同品牌的车对应具体的实现类. 如果我们想给车加入新的功能, 比如智能助手功能, 只需要给car这个类加入AI 这个接口就可以让所有品牌的车都拥有统一的AI功能. 这样可以在不改动原有类的基础关系的情况下, 加入新功能, 并降低耦合度.
- 接口中的所有成员变量都为
public static final
抽象类可以用final修饰吗, 可以被实例化吗
- 不能, 因为用final修饰的类不能被继承,而抽象类需要子类去重写抽象类的方法
- 抽象类也不能被实例化.
说一说String类
- String类是字符串对象,程序中定义""都是字符串对象,这个对象的使用频率最高.
- String类不是Java的基本类型, 是Object类. String的声明方法有两种一种是等于号直接赋值, 一种是用new声明. 然后用等号赋值是储存在字符串常量池中, 用new是将值储存在堆中
String类有一个突出特性就是不可变性, 具体体现在: 只要对string做出修改, 就会开辟新的内存空间
- 1.当对字符串重新赋值时,会重新开辟新的内存空间储存新的字符串
//常量池中会同时储存 "hello world" 和 "hello" , 而不是 "hello world" 被覆盖掉
String s = "hello world"
s = "hello"
- 2.当对现有的字符串进行连接操作时,会重新开辟新的内存空间储存拼接后的字符串
- 3.当调用String的
replace().
或者substring()
等方法修改指定字符或字符串时,也会重新开辟新的内存空间储存修改后的字符串
如何对比两个String
- 使用 == 对比两字符串时实际对比的是地址, 而不是实际内容
- 所以一律使用
s1.equals(s2)
或者s1.compareTo(s2)
对比string
public static void main(String args[]) {
String s = "hello";
String ss = "hello";
String sss = new String("hello");
System.out.println(s == ss);
System.out.println(s == sss);
System.out.println(ss == sss);
}
output:
true // 因为 直接赋值创建String时, JVM会首先在常量池中寻找是否已经有同样的字符串. 所以s和ss指向同样的地址
false //使用构造器创建String时, 地址会指向堆内存, 然后堆内存中的地址指向常量池, 所以不一样
false //同上
— String为什么要设计成不可变的
- 可以使用字符常量池
- 线程安全
String, StringBuilder和StringBuffer的区别
- String是不可变对象, 也就是对String的任何操作都会生成新的对象
- StringBuilder和StringBuffer是可变的字符串对象, 但是StringBuilder是线程不安全的, StringBuffer是线程安全的, StringBuffer对方法加入了同步锁synchronized
- 因为StringBuilder是线程不安全的, 所以有速度优势
什么是字符串常量池
- 为了减少内存开销, 字符串常量池可以避免重复创建字符串, 当需要一个字符串时, 首先在字符串常量池中寻找字符串是否已经存在
Java中的基本类型包括哪些
- Java中的基本类型包括byte, short, int, long,char, float, double, boolean等
- 其中String不是基本类型
比较包装类怎么比较
- 应该用equals, 不能用等于号
- 在缓存范围内时可以用等于号比较, 比如Integer的 -128 到 127
- 但是为了保险起见, 还是统一用equals
//下面代码会打印 not equal
Integer a = 300;
Integer b = 300;
if (a == b) {
System.out.println("equal");
}
else {
System.out.println("not equal");
}
Integer和Int的区别
- 每一个基本类型都对应有一个包装类, Integer就是包装类, int就是基本类型. 包装类是引用类型, 属于Object类, 所以可以使用集合容器, 并且值可以等于null.
包装类是什么? 基本类型和包装类型有什么区别
- 每一个基本类型都对应有一个包装类, 包装类是引用类型, 属于Object类, 所以可以使用集合容器, 并且值可以等于null
- valueOf 方法可以将基本类型转为包装类. doubleValue, intValue等方法可以将包装类转为基本类型.
- 也可以使用=号直接转换, Java自动调用上面两个方法, 被称为自动装箱和自动封箱
- 包装类还有一个特点是有缓存机制, 比如当第一次调用valueOf方法时, 会自动创建一个Integer数组储存-128到127的Integer对象, 这样下次遇到在缓存范围内的对象时, 就直接返回已经缓存的对象.
//关于缓存
public class ValuePassing {
public static void fun1(Integer i, Integer j) {
System.out.println("在fun1中赋值前i的地址是: " + System.identityHashCode(i));
System.out.println("在fun1中赋值后j的地址是: " + System.identityHashCode(j));
//对i和j同时赋予一样的值后(在-128-127之间),地址一样
i = 10;
j = 10;
System.out.println("-----------------------------------------------");
System.out.println("在fun1中赋值后i的地址是: " + System.identityHashCode(i));
System.out.println("在fun1中赋值后j的地址是: " + System.identityHashCode(j));
}
public static void main(String[] args) {
Integer i = 5;
Integer j = 20;
System.out.println("在main中i的地址是: " + System.identityHashCode(i));
System.out.println("在main中j的地址是: " + System.identityHashCode(j));
System.out.println("-----------------------------------------------");
fun1(i, j);
}
}
output:
在main中i的地址是: 1072591677
在main中j的地址是: 1523554304
-----------------------------------------------
在fun1中赋值前i的地址是: 1072591677
在fun1中赋值后j的地址是: 1523554304
-----------------------------------------------
在fun1中赋值后i的地址是: 1175962212
在fun1中赋值后j的地址是: 1175962212
什么是反射
- Java的每个class都会在虚拟机中生成一个.class文件. 在程序运行时, 可以通过.class文件得到一个类的所有信息, 包括属性, 方法等
- 反射可以在动态的创建对象实例, 提高代码灵活性
- Java中的反射通过Class, Field, Method, Constructor类获得
为什么需要泛型, 介绍下泛型
- 泛型可以解决generic programming的问题, 也就是写的代码可以适配各种类. 比如集合框架中的容器
- 在没有泛型以前, Java使用利用Object类解决泛型问题. 比如声明一个List为Object类型.
- 但是这样有两个问题, 当获取List里面的值时需要转型
- 没有Error checking, 这个list里可以同时放进所有类型的数据, 因为所有类的父类都是Object类.
- 泛型提供了type parameter, 这样就可以给泛型指定一个类型, 代码更易读, 不用转型并且有error checking
Java语言是如何处理泛型的 (介绍下类型擦除)
- Java 中的泛型只有在编译阶段存在,在代码运行的时候是没有泛型的,这也被称为泛型擦除
- 虚拟机会对泛型代码进行泛型擦除, 也就是将所有的类型 T 替换成raw type, raw type就是bound list中的第一个类型, 或者是Object类型 (如果没有bounds list)
什么是泛型中的限定通配符
- 限定通配符对类型进行了限制
- <? Extend T> 限制了泛型中的类只能是T的子类
- < ? super T> 限制了泛型中的类只能是T的父类
讲一讲Error和Exception
- Error和Exception都继承自Throwable类
- Exception
- Checked Exception: 必须使用try catch处理的exception, 包括. SQLException
- Unchecked Exception: 不用被强制的显式的抛出或者捕获. 包括Arithmetic, NullPointer, IndexOutOfBounds, Illegalargument等
- Error: 属于编译器无法检测到的错误, 比如Virtual Machine Error, Linkage Error
throw和throws的区别是什么
- throw用在方法内部, 用于抛出一种异常
- throws写在方法声明的后面, 用来标识可能抛出的方法列表. 调用该方法的方法必须包括try catch处理对应的异常, 否则需要继续使用throws抛出对应的异常.
Java常见异常有哪些
- StackoverflowError
- ClassNotFoundException
- ArthimeticException
- IndexOutOfBoundsException
- NullPointerException
try-catch-finally 中, 如果catch中有return, finally还会执行吗
- 会执行, 在return之前执行
Java变量命名规范
- 必须以字母、下划线、或者美元符$开头
- 类名第一个字母大写, 遵循驼峰原则
- 变量名, 方法名第一个字母小写, 遵循驼峰原则
i++ 和 ++i的区别
- n = i++ 是先把i的值赋给左边再对i进行递增.
- n= ++i 是先对i进行递增, 再把值赋给左边.
Object类有哪些方法
- toString(), notifyAll(), wait(), hashCode(), finalize(), clone(), equals(), getClass()
什么是hashCode以及为什么需要hashCode
- hashcode方法是Object类的方法, 也就是说所有类都有hashcode方法
- hashcode方法可以产生一个hash值. Java容器中的hashSet或者hashMap需要使用hashCode判断实例是否存在或者判断实例的位置. 具体的算法需要对hashcode产生的值进行处理(加入高位扰动等)
- 比如HashSet()的put操作, 首先计算对象的hashCode值, 判断hashtable中对应的位置是否已经有对象, 如果有则调用equals判断两个对象是否相等, 如果相等则拒绝加入, 如果不相等则通过probing放入到其他位置
hashCode和equals的关系 / 为什么重写equals一定要重写hashCode方法
- 如果两个对象hashCode不一样, 则一定不equals
- 如果两个对象hashCode一样, 有可能不equals
- 如果两个对象不equals但是hashCode可能一样
- 也就是重写equals必须重写hashCode, 否则会出现两个对象equals但是hashCode不一样的情况, 导致两个equals的对象被同时加入hash
如何判断double的0值
- 因为double 在运算中,由于截尾的原因,总是有误差的。而此时是否为0,要看你的这个运算的精度要求。
- 比如运算后,f = 0.001,需要和规定精度比较,比如你的精度要求是0.000001,则认为 f 值不为0,若精度为0.01,则认为 f 就等于0
- 可以这样 if(abs(f) < 精度0.000001) 执行 f 等于0时的操作
— double和float是如何在内存中储存的
2. Java 集合框架
说一说Java集合框架常用的类
- 集合框架分为两个部分, 分别为Collection和Map
- Collection里常用的类有, LinkedList, ArrayList, HashSet, TreeSet, ArrayQueue
- Map里常用的类有HashMap, TreeMap
说一下HashMap的底层原理实现
说一下StringBuilder, StringBuffer的区别
- StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)
- 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。
Comparator和Comparable的区别
线程安全的集合有哪些
- HashTable, ConcurrentHashMap, Vector, Stack
- 优先使用ConcurrentHashMap