从笔者个人角度, 针对Java基础中的一些生僻难点进行了归类整理, 希望可以帮到有需要的兄弟们
1 关键字
1.1 private
类中所有的private
方法都隐式地指定为final
1.2 static
static
修饰的变量永远不会被序列化
类中被static
修饰的内容建议通过类名进行调用
通过静态导入import static
导入的静态成员, 可以直接使用, 不需要再通过类名调用
1.3 strictfp
strictfp
关键字可以应用于方法/类/接口, 不能应用于抽象方法/变量/构造函数, 确保在每个平台上获得相同的结果
常用于浮点数相关内容, 防止因平台不同导致数据精度不一致
Java 17中解决了浮点指令问题, 已移除此关键字
1.4 transient
对于不想序列化的变量, 使用transient
需要注意以下内容:
- 只能修饰变量, 不能修饰类和方法
- 修饰的变量, 在反序列化后变量值将会被置成类型的默认值. 例如, 如果是修饰
int
类型,那么反序列化后结果就是0
2 基本类型
2.1 8种基本类型介绍
基本类型 | 位数 | 字节 | 默认值 |
---|---|---|---|
int | 32 | 4 | 0 |
short | 16 | 2 | 0 |
long | 64 | 8 | 0L |
byte | 8 | 1 | 0 |
char | 16 | 2 | 'u0000' |
float | 32 | 4 | 0f |
double | 64 | 8 | 0d |
boolean | 1 | false |
其中boolean依赖于JVM厂商的具体实现, 理论上占1位, 可能出现不同
2.2 精度丢失问题
float
和double
都会丢失精度, 原因是数据转换为二进制可能会出现无限循环, 超出储存长度
在涉及金额等极为敏感的浮点数据时, 请使用BigDecimal
来处理运算
在创建BigDecimal
时, 应使用字符串参数或valueOf
方法:
// YES!!
new BigDecimal("0.1");
BigDecimal.valueOf(0.1f);
// NO!! 会丢失精度
new BigDecimal(0.1f);
复制代码
2.3 包装类型
基本类型都有对应的包装类型, 其存在的意义在于允许null值含义
举个例子, 学生参加考试但成绩为0和没有参加考试成绩为null是两种情形, 基本类型int
没办法体现, 其包装类型Integer
则可以
包装类型除Float
和Double
外都存在常量池, 所以在比较时应使用equals
方法而不是==
:
// 常量池中存在一个Integer对象, 其值为1
Integer i1 = 1;
Integer i2 = 1;
Integer i3 = new Integer(1);
// 结果为true
i1 == i2;
// 结果为false
i1 == i3;
// 结果为true
i1.equals(i3);
复制代码
3 函数
3.1 函数签名
函数签名是函数在一个类中的唯一标识
内容包括方法名/参数类型/参数名, 不包括返回值
3.2 重载
发生在同一个类中(或者父类和子类之间), 方法名必须相同,参数类型不同/个数不同/顺序不同,方法返回值和访问修饰符可以不同
那可以存在方法名相同/参数相同但返回值不同的重载吗?
答案是不行, 3.1中的函数签名不包括返回值, 所以方法名相同/参数相同但返回值不同的方法会被认为是同一方法, 不能进行重载
3.3 重写
方法名/参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类
如果方法的返回值类型是 void 和基本数据类型,则返回值重写时不可修改
如果方法的返回值类型是引用类型,重写时可以返回该引用类型的子类
3.4 try-catch-finally
当try
语句和finally
语句中都有return
时, finally
语句的返回值将会覆盖原始的返回值
换而言之, finally
语句一定会执行
// 返回为0
try {
return 1;
} catch (Exception e) {
// ...
} finally {
return 0;
}
复制代码
3.5 try-with-resource
适用于任何实现 java.lang.AutoCloseable
或者 java.io.Closeable
的对象, 会自动调用close
方法
4 面向对象
4.1 对象构建顺序
- 静态代码块
- 非静态代码块
- 构造方法
4.2 静态代码块
public class Person {
static {
// 静态代码块
}
}
复制代码
静态代码只会在类的初始化步骤执行一次, 具体在代码中写法为:
new Person()
Class.forName("cn.houtaroy.models.Person")
举个具体的例子:
package cn.houtaroy.models;
public class Person {
static {
System.out.println("静态代码块执行");
}
public static void main(String[] args) {
try {
new Person();
Class.forName("cn.houtaroy.models.Person");
new Person();
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
上述代码只会有一次输出: 静态代码块执行
可以理解为: 静态代码块中定义的是不同对象共性的初始化内容
一个类中的静态代码块可以有多个,会按照它们出现的先后顺序依次执行
public class Person {
static {
// 先执行
}
static {
// 再执行
}
}
复制代码
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问:
public class Person {
static {
// 可以赋值
age = 0;
// IDE报错, 无法访问
System.out.printf("出生年龄: %d", age);
}
public static Integer age;
}
复制代码
4.3 静态内部类
- 不需要依赖外围类的创建
- 不能使用任何外围类的非静态成员变量和方法
静态内部类可用于实现单例模式, 优点是延迟初始化和JVM提供的线程安全支持 :
public class PersonFactory {
// 私有构造方法, 防止在外部调用
private PersonFactory() {
}
// 私有静态内部类, 无法被外部访问
private static class PersonFactoryHolder {
private static final PersonFactory INSTANCE = new PersonFactory();
}
// 静态方法, 调用私有静态内部类获取唯一实例
public static PersonFactory getInstance() {
return PersonFactoryHolder.INSTANCE;
}
}
复制代码
4.4 hashCode
与equals
hashCode
用于判断对象的hash值是否相同
equals
用于判断对象是否相同
在用到hash的地方都会使用hashCode
和equals
, 例如HashMap
. 所以在重写时, 请务必使二者的返回结果保持一致
4.5 值传递
Java中只有值传递, 哪怕是在面向对象, 举个具体的例子:
public class Test {
public static void main(String[] args) {
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
复制代码
输出结果为:
x:小李
y:小张
s1:小张
s2:小李
复制代码
可以看到, 在函数内部x
和y
进行了交换, 但外部的s1
和s2
并没有发生变化
所以这四个变量代表的含义是:
s1
: 学生小张对象的引用的值s2
: 学生小李对象的引用的值x
: s1的深拷贝y
: s2的深拷贝
因为x
和y
是深拷贝, 所以无论它们如何变化, 都不会影响原来的s1
和s2
4.6 this
/super
与静态
两者的概念范畴完全不同:
- 静态方法是类范畴的概念
this
/super
是对象范畴的概念
4.7 Objects.equals
在进行比较时(并非类, 全部内容均可)推荐使用Objects.equals
:
// 均为false, 且不会抛空指针异常
Objects.equals("Houtaroy", null);
Objects.equals(null, "Houtaroy");
复制代码
5 枚举
5.1 使用枚举相关集合
在将枚举作为元素或键值等使用时, 推荐使用枚举相关集合, 例如EnumSet
和EnumMap
:
// 创建EnumSet
EnumSet<Gender> set = EnumSet.of(Gender.MAN);
// 创建EnumMap
EnumMap<Gender, String> map = new EnumMap<>(Gender.class);
map.put(Gender.MAN, "男人");
复制代码
5.2 实现设计模式
单例模式
利用枚举可以更简洁/高效/安全的实现单例模式, 且由JVM提供保障
public enum PersonFactory {
INSTANCE;
PersonFactory() {
// 实现人员工厂初始化
person = PersonCheckEntity.builder().id("test").build();
}
private PersonCheckEntity person;
public static PersonFactory getInstance() {
return INSTANCE;
}
public PersonCheckEntity getDeliveryStrategy() {
return this.person;
}
}
复制代码
策略模式
public enum PersonStrategy {
STAND {
@Override
public void advance(Person person) {
System.out.println("向前迈了一步");
}
},
SIT {
@Override
public void advance(Person person) {
System.out.println("向前爬了一截");
}
};
public abstract void advance(Person person);
}
public class Person {
private PersonStrategy status;
public void advance() {
status.advance(this);
}
}
复制代码
状态模式
public enum PersonStrategy {
STAND {
@Override
public void walk(Person person) {
System.out.println("向前迈了一步");
}
},
SIT {
@Override
public void walk(Person person) {
System.out.println("坐着没办法走路, 站起来");
person.setStatus(PersonStrategy.STAND);
person.walk();
}
};
public abstract void walk(Person person);
}
public class Person {
private PersonStrategy status;
public void walk() {
status.walk(this);
}
}
复制代码
6 反射
6.1 什么是反射
反射是框架的灵魂
它赋予了程序在运行过程中分析和使用类概念的能力, 使我们脱离最基本的业务逻辑, 站在更高的维度去处理和思考问题
Java中的利器注解便用到了反射
6.2 获取Class
对象的四种方式
Class
对象可以理解为类的描述
具体类
Class myClass = Target.class;
复制代码
Class.forName
Class myClass = Class.forName("cn.houtaroy.models.Target");
复制代码
对象实例
Target object = new Target();
Class myClass = object.getClass();
复制代码
类加载器
Class myClass = ClassLoader.loadClass("cn.houtaroy.models.Target");
复制代码
通过类加载器获取的Class不会执行初始化, 意味着不进行包括初始化等一系列步骤,静态块和静态对象不会执行
6.3 具体操作
创建目标类:
public class Target {
private String value;
public void say(String name) {
System.out.printf("I love %s%n", name);
}
private void classValue() {
System.out.printf("value is %s%n", value);
}
}
复制代码
进行反射操作:
public class ExampleUtils {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException,
NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class<?> targetClass = Class.forName("cn.houtaroy.models.Target");
Object targetObject = targetClass.newInstance();
Method[] methods = targetClass.getMethods();
for (Method method : methods) {
System.out.printf("拥有方法: %s%n", method.getName());
}
Field[] fields = targetClass.getFields();
for (Field field : fields) {
System.out.printf("拥有字段: %s%n", field.getName());
}
targetClass.getDeclaredMethod("say", String.class).invoke(targetObject, "Java");
Field valueField = targetClass.getDeclaredField("value");
valueField.setAccessible(true);
valueField.set(targetObject, "test");
Method classValueMethod = targetClass.getDeclaredMethod("classValue");
classValueMethod.setAccessible(true);
classValueMethod.invoke(targetObject);
}
}
复制代码
运行结果为:
拥有方法: say
拥有方法: wait
拥有方法: wait
拥有方法: wait
拥有方法: equals
拥有方法: toString
拥有方法: hashCode
拥有方法: getClass
拥有方法: notify
拥有方法: notifyAll
I love Java
value is test
复制代码
getMethods
和getFields
方法只会读取public
属性- 访问私有属性时使用
getDeclaredMethod
和getDeclaredField
, 并调用setAccessible(true)
修改其可使用性
从上述内容即可看出, 利用反射会带来一定程度的安全隐患
7 Java中的IO
程序I/O可分解为如下操作:
- 程序向操作系统发起I/O调用请求
- 系统内核等待I/O设备准备好数据
- 系统内核将数据从内核空间拷贝到用户空间
7.1 Blocking I/O
即同步阻塞I/O, 程序会一直等待到I/O操作执行完成
7.2 Non-blocking I/O
即I/O多路复用模型
同步非阻塞I/O使用轮询方式, 会非常消耗CPU资源, 在Web开发中几乎无法使用
I/O多路复用模型利用一个线程来管理, 通过减少无效的系统调用,减少了对 CPU 资源的消耗
7.3 Asynchronous I/O
即异步I/O模型, 通俗点就是系统内核完成后会执行程序指定的回调函数
8 小细节
8.1 StringBuilder
与StringBuffer
区别
StringBuilder
线程不安全
StringBuffer
线程安全
8.2 字节流与字符流
字节(Byte)是计量单位,表示数据量多少,是计算机信息技术用于计量存储容量的一种计量单位,通常情况下一字节等于八位
字符(Character)是计算机中使用的字母、数字、字和符号, 在不同的编码中占用的字节数量不同
Java中存在字节流为什么还要提供字符流呢?
因为在不知道字符编码类型时, 使用字节流很容易出现乱码问题
音频文件、图片等媒体文件用字节流较好,如果涉及到字符的话使用字符流较好