Java 语法与面向对象
配套知识点:01 Java 基础、JDK 新特性与升级
怎么用这份笔记
- 分级:
★★★ 必须吃透/★★☆ 了解即可(记结论就行)/★☆☆ 可以跳过(一句话看一眼)- 【简历亮点,高频追问】:简历里写过的技术,面试官会顺着往下挖,这段必须能展开讲
- 每个考点按「定义 → 原理 → 要点 / 易错点」组织,只讲知识点
1. == 与 equals、hashCode 契约 ★★★
== 判断「是不是同一个对象」:基本类型比值,对象比地址。equals 默认也是比地址,但 String、Integer 等类把它重写成「内容相同即相等」。
hashCode 是对象在哈希表里的「门牌号」,供 HashMap 定位桶使用。
契约:两个对象 equals 相等,则 hashCode 必须相等;反过来 hashCode 相等不保证 equals 相等。
- 自定义对象只要会放进 Map / Set(去重、缓存),
equals与hashCode必须成对重写。 - 只重写
equals不重写hashCode:两个equals相等的对象hashCode不同,会落到不同桶,导致Set能塞进重复元素、Mapget 不到。 - 只重写
hashCode不重写equals:哈希碰撞后仍按地址比较,同样会出问题。 - 两者必须基于同一组字段计算,参与
equals的字段也要参与hashCode,否则契约不成立。
2. String 不可变 / 常量池 / StringBuilder ★★★
String 创建后内容不可变,每次「修改」都是新建一个对象,原对象内容不变。
不可变带来的三个结论:
- 可以安全共享,多线程读不需要同步;
- 适合当 HashMap 的 key:hash 值在生命周期内不变,不会出现「放进去后 get 不到」;
- 字符串常量池可以复用同一份字面量,字面量直接指向池中对象。
拼接的选择:
- 频繁拼接用 StringBuilder:快、线程不安全,适合方法内部的局部变量;
- 多线程共享同一个拼接对象时用 StringBuffer:方法加
synchronized,线程安全但有锁开销。
字符串常量池是 String 的公共仓库。new String("a") 会产生两个对象——一个在常量池、一个在堆,日常代码不会这么写。
JDK 9 之后 String 底层从 char[] 换成 byte[] 加 coder 标记(Latin-1 时每字符 1 字节,其他编码 2 字节),省一半左右空间。
3. 泛型擦除 / 通配符 PECS ★★★
泛型只在编译期存在,编译完成后类型参数被擦除:List<String> 和 List<Integer> 运行时是同一个类,因此拿不到运行时的泛型实参。
擦除规则:
- 无界类型参数擦成
Object; - 有上界的类型参数擦成上界,例如
T extends Number擦成Number; - 这也是泛型数组不能 new(
new T[n])的原因:运行时不知道具体类型,无法做数组类型检查。
通配符口诀 PECS(Producer Extends, Consumer Super):
? extends T:只能读不能写,作为「生产者」向外提供 T 类型的数据;写入会被编译器拒绝(null 除外)。? super T:能写,作为「消费者」接收 T 类型的数据;读出来只能当作Object。
4. BigDecimal 与浮点精度 ★★★ 【简历亮点,高频追问】
float / double 是二进制浮点,像 0.1 这样的十进制小数在二进制里是无限循环,只能存近似值,所以 0.1 + 0.2 != 0.3(实际是 0.30000000000000004)。金额、电量、费率这类精确计算必须用 BigDecimal。
BigDecimal 的三个坑:
- 构造必须传字符串:
new BigDecimal("0.1");传 double 会把浮点误差原样带进来。 - divide 必须指定精度或舍入模式:除不尽时会抛
ArithmeticException,应写成divide(divisor, scale, RoundingMode.HALF_UP);按时间单位折算费率这类除法最容易除不尽。 - 比较用 compareTo 而不是 equals:
equals同时比较数值和 scale,1.0与1.00不相等;compareTo只比数值。
5. 值传递(前端转全栈必考)★★★
Java 只有值传递,没有引用传递。
- 基本类型:传的是值的副本,方法内怎么改都影响不到外面。
- 引用类型:传的是「地址的副本」,副本与原引用指向同一个对象,因此:
- 方法内修改对象字段,外部能看到;
- 方法内把引用重新指向新对象,或对两个引用做
swap,外部看不到,因为改的只是副本。
易错点:swap(a, b) 对引用类型无效,交换的只是两个副本;需要交换时可以返回新引用,或用数组 / 容器承载结果。
6. 类初始化顺序 ★★☆
类初始化顺序:
- 父类静态(静态变量、静态代码块,按源码先后顺序)
- 子类静态
- 父类实例变量 / 实例代码块
- 父类构造器
- 子类实例变量 / 实例代码块
- 子类构造器
口诀:先静态后实例、先父后子。
- 静态部分只在类第一次主动使用时执行一次,之后不再执行。
new 子类会先触发父类的初始化,再初始化子类。- 静态块适合放全局配置、注册逻辑;实例块做每个实例各自的初始化,所以静态一定早于实例。
7. 不可变类 ★★★
不可变类的四个步骤:
- 类声明为
final,禁止被继承(防止子类改变不可变语义); - 所有字段
private final; - 不提供
setter,只提供 getter; - 可变字段(数组、集合、
Date)在构造时和 getter 时都做防御性拷贝,避免外部拿到内部引用后直接修改。
典型实现:String、Integer、BigDecimal、LocalDate / LocalDateTime。
好处:线程安全(状态不可变,天然没有并发问题)、可以被安全共享、能当 HashMap 的 key(hash 不变)。
易错点:只加 final 不够——final 只保证引用不变,不保证引用指向的对象内容不变,集合 / 日期字段必须防御性拷贝。
8. hash 冲突的解决办法 ★★★
常见四种 hash 冲突解决办法:
- 链地址法:冲突的元素挂成链表,HashMap 用这种。实现简单、删除方便;JDK 8 起链表长度达到 8 且桶数组长度达到 64 时转红黑树,把最坏查询从 O(n) 降到 O(log n)。
- 开放定址法:冲突后按规则往后探测空位,ThreadLocalMap 用线性探测;此外还有二次探测、双重散列。
- 再哈希(双哈希):准备多个哈希函数,冲突时依次换下一个,直到找到空位。
- 公共溢出区:冲突元素统一放到另一个溢出表。
易错点:开放定址法删除元素需要特殊标记,直接置空会截断后续探测链;链地址法没有这个问题,删除更方便。
9. 虚拟线程 vs 平台线程 ★★★ 【简历亮点,高频追问】
- 平台线程:1:1 绑定操作系统线程,创建和切换成本高,数量受限于内核线程数,几千个就到头。
- 虚拟线程:JVM 调度的 M:N 模型,大量虚拟线程挂在少量**载体线程(carrier thread)**上。遇到 IO 阻塞时自动卸载,载体线程立刻去跑别的任务。
适用场景:
- 只适合 IO 密集型(等数据库、等下游 HTTP、等 Redis);
- CPU 密集型用虚拟线程没有收益,还多一层调度开销。
落地时要注意两点:
- 连接池并发上限不会跟着变:线程数上去了,数据库连接池还是原来的大小,真正要控的是连接占用时长,别让连接被长时间占着。
synchronized包着 IO 会 pin 住载体线程(JDK 21 中 synchronized 阻塞不卸载),这类代码应改成 ReentrantLock。
JDK 21 起虚拟线程为正式特性,Executors.newVirtualThreadPerTaskExecutor() 可以做到每个任务一个虚拟线程。
10. JDK 版本演进 8/11/17/21 ★★☆
- Java 8:Java 的分水岭——Lambda、Stream、Optional、新时间 API、接口 default / static 方法。
- Java 11:
var局部变量类型推断、新的 HTTP Client(java.net.http)、String 新增isBlank/strip/repeat等。 - Java 17:
record记录类、sealed密封类、文本块、instanceof模式匹配、switch表达式。 - Java 21:虚拟线程正式、
switch模式匹配正式、分代 ZGC。
选型提示:新服务直接用 Java 21 + Spring Boot 3.x,可以跳过 Java 8 到 17 之间的兼容性包袱;新时间 API 统一用 LocalDateTime,不用老的 Date。
11. 枚举 ★★☆
枚举本质是继承 java.lang.Enum 的 final 类,每个枚举常量都是该类的唯一实例,构造器由编译器强制为私有。
由此带来的特性:
- 天然防反射:反射创建枚举实例会抛异常,无法 new 出第二个实例;
- 天然防反序列化破坏:反序列化按名称查找已有常量,不会新建对象;
- 所以枚举是实现单例最稳的方式。
易错点:枚举可以带字段、构造器、方法,也可以实现接口,但不能再继承其他类(已隐式继承 Enum)。常量之间直接用 == 比较即可,不需要 equals。
常见用法:表达固定集合,如订单状态、设备状态、协议类型、支付渠道、故障码;枚举里挂 code 加中文描述字段是常规写法;状态机的流转合法性判断也常基于枚举。
12. Object 常用方法 ★★☆
Object 是所有类的根,常用方法如下:
equals:默认比地址,子类通常重写为比内容;hashCode:哈希查找用,重写equals必须同时重写hashCode;toString:默认输出「类名@十六进制 hash」,日志排查时通常重写为可读的字段内容;clone:受保护方法,需实现Cloneable才能调用,否则抛CloneNotSupportedException;默认是浅拷贝,深拷贝要自己处理引用类型字段;getClass:返回运行时Class对象,final方法,不能重写;wait/notify/notifyAll:线程协作,必须配合synchronized使用,调用前必须持有该对象的锁,否则抛IllegalMonitorStateException。
finalize 已废弃,不要依赖它做资源释放,应使用 try-with-resources 或显式 close。
注意:wait 会释放锁并进入等待队列,sleep 不释放锁;notify 只随机唤醒一个等待线程,notifyAll 唤醒全部;wait 被中断会抛 InterruptedException。
13. 四种内部类 ★☆☆
一句话:成员 / 静态 / 局部 / 匿名四种。业务里常用静态内部类(Builder、静态工具)和匿名内部类(现在基本被 Lambda 替代)。关键结论——静态内部类不持有外部引用,不会内存泄漏。
14. 重载与重写 ★☆☆
一句话:重载看参数列表、编译期决定、同一个类里;重写看方法签名一致、运行期决定、父子类之间。只返回值不同不算重载,静态方法不能被重写只会被隐藏。
15. 抽象类和接口的异同(Java 8 之后接口默认方法/静态方法的变化)★★★
抽象类是「is-a」关系,是类的模板:
- 可以有成员变量、构造器、普通方法、抽象方法;
- 单继承,一个类只能继承一个抽象类;
- 管「是什么、有什么状态」。
接口是「can-do」关系,是行为的契约:
- 字段默认
public static final; - 方法默认
public abstract; - 多实现,一个类可以实现多个接口;
- 管「能做什么」。
Java 8 之后接口的变化:
- default 方法:带默认实现,实现类可覆盖可不覆盖。目的是接口演进——给已有大量实现类的接口加新方法时,老实现类不用改,避免全部编译报错。
- static 方法:属于接口本身,只能通过「接口名.方法名」调用,实现类不能继承也不能覆盖。用来放和接口强相关的工厂方法 / 工具方法,例如
ProtocolConverter.create(type)返回对应实现。
对比记忆:default 方法是给实现类「继承」用的,static 方法是给调用方「直接用」的。
易错点:接口不能有实例字段和构造器;多个接口的 default 方法签名冲突时,实现类必须显式覆盖,并用 接口名.super.方法名() 指定调用哪一个。
16. 使用 Java 8 的优点 ★★★
Java 8 是 Java 最大的分水岭,三个核心价值:
- Lambda:让代码从「怎么做」变成「做什么」,把匿名内部类写成一行;配套方法引用
::。 - Stream:集合操作变成声明式、可链式、能并行,
filter/map/groupingBy组合表达意图,可读性和正确性都更高。 - 新时间 API(
LocalDate/LocalDateTime/Duration):不可变、线程安全、API 清晰,替代线程不安全且 API 混乱的老Date/SimpleDateFormat。
配套特性:
- 接口 default / static 方法;
- Optional:显式表达「可能为空」,减少空指针,配合
orElse/orElseGet/ifPresent; - CompletableFuture:异步编排,支持
thenCombine/allOf,可用于对多个下游并发调用并合并结果; - 方法引用、重复注解、
Base64、Metaspace取代永久代。
实际收益:集合处理和异步调用的代码量明显下降;LocalDateTime 处理时间字段、按小时切分时间段(如分时电价按小时切分)也很自然。
17. Predicate 和 Consumer 等常用函数式接口的区别与用法 ★★★
Java 8 在 java.util.function 包中提供了一组标准函数式接口,直接配 Lambda 使用即可,不必自己造接口:
- Predicate:
boolean test(T t),接收一个参数返回 boolean,用来「判断」;默认方法有and/or/negate,可以组合条件。 - Consumer:
void accept(T t),接收一个参数没有返回值,用来「消费 / 执行操作」;andThen可以串联多个操作。 - Function:
R apply(T t),接收 T 返回 R,用来「转换」;andThen/compose可以组合函数。 - Supplier:
T get(),无参返回一个值,用来「供应」;Optional.orElseGet用它实现惰性兜底。 - BiFunction / BiConsumer:两个参数版本;
UnaryOperator是Function的同类型特化,BinaryOperator是BiFunction的同类型特化。
与 Stream 的对应关系:
filter传 Predicate,就是过滤条件;map传 Function,做实体转 DTO、从对象中提取 key / value;forEach传 Consumer,做批量操作、打日志;Optional.orElseGet、Stream.generate传 Supplier。
易错点:orElse 无论是否为空都会先求值,orElseGet 才惰性;判断条件需要在多处复用时,应提成变量或方法,例如 Predicate<Order> isTestOrder = o -> TEST.equals(o.getSource());,比在 filter 里重复写一长串 lambda 可读性更好。
18. 接口中的静态方法有什么作用 ★★☆
Java 8 允许在接口里写 static 方法,和类的 static 方法一样:
- 属于接口本身,不属于实现类实例;
- 只能通过「接口名.方法名」调用;
- 实现类不能继承也不能覆盖,用实例方式或
super.都调不到。
作用只有一个:放「和接口强相关、但不需要实例状态」的工具方法或工厂方法,避免再单独建一个 Factory / Utils 类,让「接口 + 它的配套工具」放在同一个文件里。
典型用法:静态工厂 ProtocolConverter.create(type),根据类型返回对应实现,调用方不用关心实现类是谁;以及紧贴接口语义的校验方法,如标识符格式校验。
对比记忆:
- default 方法:给实现类「继承」用,可被覆盖;
- static 方法:给调用方「直接用」用,只能接口名调用。
这也体现了 Java 8 之后接口从「纯契约」向「契约 + 配套工具」的演进。
附:原始题库(旧版逐题,与上文重叠,可折叠跳过)
展开查看原始题库
19. Java语言有哪些特点
1、简单易学、有丰富的类库
2、面向对象(Java最重要的特性,让程序耦合度更低,内聚性更高)
3、与平台无关性(JVM是Java跨平台使用的根本)
4、可靠安全
5、支持多线程
20. 面向对象和面向过程的区别
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤, 而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。
21. 八种基本数据类型的大小,以及他们的封装类
| 基本类型 | 大小(字节) | 默认值 | 封装类 |
|---|---|---|---|
| byte | 1 | (byte)0 | Byte |
| short | 2 | (short)0 | Short |
| int | 4 | 0 | Integer |
| long | 8 | 0L | Long |
| float | 4 | 0.0f | Float |
| double | 8 | 0.0d | Double |
| boolean | - | false | Boolean |
| char | 2 | \u0000(null) | Character |
注:
1.int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
2.基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间, 必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。
虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java 虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素
boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的
是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
22. 标识符的命名规则。
标识符的含义: 是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。
命名规则:(硬性要求) 标识符可以包含英文字母,0-9的数字,$以及_ 标识符不能以数字开头标识符不是关键字
命名规范:(非硬性要求) 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量
名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
23. instanceof 关键字的作用
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
24. Java自动装箱与拆箱
装箱就是自动将基本数据类型转换为包装器类型(int-->Integer);调用方法:Integer的 valueOf(int) 方法
拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。调用方法:Integer的 intValue方法
在Java SE5之前,如果要生成一个数值为10的Integer对象,必须这样进行:
而在从Java SE5开始就提供了自动装箱的特性,如果要生成一个数值为10的Integer对象,只需要这样就可以了:
面试题1: 以下代码会输出什么?
| public class Main {public static void main(String[] args) { |
|---|
| Integer Integer Integer Integer |
| System.out.println(i1==i2);System.out.println(i3==i4);}} |
运行结果:
为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:
其中IntegerCache类的实现为:
private static class IntegerCache {
static final int high;
static final Integer cache[];private static class IntegerCache {
static final int high;
static final Integer cache[];static {
final int low = -128;static {
final int low = -128;// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that // require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue(); i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that // require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue(); i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}private IntegerCache() {}
}private IntegerCache() {}
}从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,
便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一个对象,而i3和i4则是分别指向不同的对象。
面试题2:以下代码输出什么
| public class Main {public static void main(String[] args) { |
|---|
| Double Double DoubleDouble |
| System.out.println(i1==i2);System.out.println(i3==i4);}} |
运行结果:
原因: 在某个范围内的整型数值的个数是有限的,而浮点数却不是。
25. 重载和重写的区别
重写(Override)
从字面上看,重写就是重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名, 参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
public class Father {public class Father {public static void main(String[] args) {
Son s = new Son();
s.sayHello();
}public static void main(String[] args) {
Son s = new Son();
s.sayHello();
}public void sayHello() {
System.out.println("Hello");
}
}public void sayHello() {
System.out.println("Hello");
}
}class Son extends Father{class Son extends Father{@Overridepublic void sayHello() {// TODO Auto-generated method stubSystem.out.println("hello by ");}}
重写总结: 1.发生在父类与子类之间 2.方法名,参数列表,返回类型(除过子类中方法的返回类型
是父类中返回类型的子类)必须相同 3.访问修饰符的限制一定要大于被重写方法的访问修饰符
(public>protected>default>private) 4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
重载(Overload)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
重载总结: 1.重载Overload是一个类中多态性的一种表现 2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序) 3.重载的时候,返回值类型可以相同也可以不相同。无法以返回
型别作为重载函数的区分标准
26. equals与==的区别
==:
== 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。比较的是真正意义上的指针操作。
1、比较的是操作符两端的操作数是否是同一个对象。 2、两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过。 3、比较的是地址,如果是具体的阿拉伯数字的比较,值相等则为
true,如: int a=10 与 long b=10L 与 double c=10.0都是相同的(为true),因为他们都指向地址为10的堆。
equals:
equals用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object 中的equals方法返回的却是==的判断。
总结:
所有比较是否相等时,都是用equals 并且在对常量相比较时,把常量写在前面,因为使用object的 equals object可能为null 则空指针
在阿里的代码规范中只使用equals,阿里插件默认会识别,并可以快速修改,推荐安装阿里插件来
排查老代码使用“==”,替换成equals
27. Hashcode的作用
java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set 中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就会比较满。
于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。
hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
10、 String、 String StringBuffer 和 StringBuilder 的区别是什
28. Java的四种引用,强弱软虚
强引用
强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
软引用
软引用在程序内存不足时,会被回收,使用方式:
可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
弱引用
弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
key java.util.WeakHashMap 可用场景: Java源码中的中的就是使用弱引用,我的理解就是,
一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
虚引用
ReferenceQueueReferenceQueue虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入中。注意哦,其它引用是被JVM回收后才被传入中的。由于这个机制,所以虚引用大多
ReferenceQueue,被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有
使用例子:
可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
上诉所说的几类引用,都是指对象本身的引用,而不是指Reference的四个子类的引用 (SoftReference等)。
29. 泛型常用特点
泛型是Java SE 1.5之后的特性, 《Java 核心技术》中对泛型的定义是:
I “泛型” 意味着编写的代码可以被不同类型的对象所重用。
“泛型”,顾名思义, “泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如 Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素,如
使用泛型的好处?
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
30. Java创建对象有几种方式?
java中提供了以下四种创建对象的方式:
new创建新对象
通过反射机制
采用clone机制
通过序列化机制
补充(原文详解)
这题目看似简单,要好好回答起来还是有点小复杂的,我们来看看,到底有哪些方式可以创建对象?
使用 new 关键字,这也是我们平时使用的最多的创建对象的方式,示例:
使用反射方式创建对象,使用 newInstance(),但是得处理两个异常 InstantiationException、 IllegalAccessException:
使用 clone 方法,前面题目中 clone 是 Object 的方法,所以所有对象都有这个方法。
使用反序列化创建对象,调用 ObjectInputStream 类的 readObject() 方法。
我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。 一个对象实现了 Serializable 接口,就可以把对象写入到文件中,并通过读取文件来创建对
象。
总结
创建对象的方式关键字: new、反射、clone 拷贝、反序列化。
31. 有没有可能两个不相等的对象有相同的hashcode
有可能.在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般有以下几种方式来处理:
拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个 ….等哈希函数计算地址,直到无冲突.
32. final有哪些用法?
final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了:
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变. 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
被final修饰的常量,在编译阶段会存入常量池中.
除此之外,编译器对final域要遵守的两个重排序规则更好:
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作
之间不能重排序初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.
33. static都有哪些用法?
所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/ 方法都属于类的静态资源,类实例所共享.
除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作:
此外static也多用于修饰内部类,此时称之为静态内部类.
最后一种用法就是静态导包,即 import static .import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如:
34. 3*0.1 == 0.3 的返回值是什么
false,因为有些浮点数不能完全精确的表示出来.
35. a=a+b与a+=b有什么区别吗?
+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换.如:
以下代码是否有错,有的话怎么改?
有错误.short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是 short类型,此时编译器会报错.
正确写法:
+=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.
36. OOM你遇到过哪些情况,SOF你遇到过哪些情况
OOM:
1,OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java Heap 溢出:
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
2,虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
这里需要注意当栈的大小越大可分配的线程数就越少。
3,运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGenspace
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对
象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
4,方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
异常信息:java.lang.OutOfMemoryError:PermGenspace
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
SOF(堆栈溢出StackOverflow):
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容
量超过1m而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、 List、 map数据过大。
37. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是一个比进程更小的执行单位。 一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存
中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
38. Object 有哪些常用方法?大致说一下每个方法的含义
java.lang.Object
下面是对应方法的含义。
clone 方法
保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出
CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。
finalize 方法
该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。
equals 方法
该方法使用频率非常高。一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。
hashCode 方法
该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的 Collection 中用到。
一般必须满足
。可以推出
,但是
hashCode 相等不一定就满足 equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
JDK 1.6、 1.7 默认是返回随机数;
· JDK 1.8 默认是通过和当前线程有关的一个随机数 + 三个确定值,运用 Marsaglia’s xorshift scheme 随机数算法得到的一个随机数。
wait 方法
配合 synchronized 使用,wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(longtimeout)
设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
其他线程调用了该对象的 notify 方法;
其他线程调用了该对象的 notifyAll 方法;
其他线程调用了 interrupt 中断该线程;
时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。
notify 方法
配合 synchronized 使用,该方法唤醒在该对象上等待队列中的某个线程(同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程)。
notifyAll 方法
配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。
总结
只要把上面几个方法熟悉就可以了,toString 和 getClass 方法可以不用去讨论它们。该题目考察的
是对 Object 的熟悉程度,平时用的很多方法并没看其定义但是也在用,比如说:wait() 方法, equals() 方法等。
大致意思:Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。
附:一句话速记(可以跳过 ★☆☆)
- equals / hashCode:重写 equals 必须重写 hashCode,否则 HashMap / HashSet 行为异常。
- String 不可变:final 字符数组 + 类 final,常量池复用;拼接多时用 StringBuilder。
- 泛型擦除:编译后变 Object + 强转,所以不能
new T[]、静态方法不能用类泛型;通配符记住 PECS。 - 值传递:Java 只有值传递,传对象传的是引用的副本,改对象内容生效、重新赋值不生效。
- 四种引用:强(不回收)、软(内存不足回收,做缓存)、弱(下次 GC 必回收,ThreadLocal)、虚(回收通知)。
- 类初始化顺序:父静态 → 子静态 → 父成员 → 父构造 → 子成员 → 子构造。
- BigDecimal:用 String 构造,比较用 compareTo,别用 equals(比 scale)。
- 接口 vs 抽象类:接口多实现、无状态;抽象类单继承、可存状态;Java 8 后接口可有 default / static 方法。
Cao Note