20分钟了解Java
Java基础
介绍
本文基于Java全栈知识体系和JavaGuide进行总结扩展,旨在快速理解Java基础的核心思想。建议阅读模式是看一遍,重于理解。忘了也没关系,之后用到再回来搜索即可。
Java学习路径 : Java基础的核心思想 -> Java容器 -> Java多线程与并发 -> Java IO/NIO/AIO -> Java设计模式 -> Java应用
面向对象
三大特性
- 封装 : 包装数据。优点是 a)减少耦合、例如getter、setter,外部使用getter的时候,在变量需要变更时,我们能在getter方法内部做些变更。b)保护数据,例如变量password。 c)调解性能,可以内部变更去优化性能。提高软件可用性之类的操作都差不多。主要要去判断哪些数据需要被保护,哪些需要被放开,需要被保护的就封装起来,例如密码、身份证号码之类的,具体实现就是变量私有化,提供一个公共方法供外部使用。
1
2
3
4
5
private String name;
public String getName() {
return name;
}
- 继承 : 需要遵守里氏替换原则,也就是任何父类(基类)可以出现的地方,子类一定可以出现。为什么要遵守这个原则?因为继承关系会给程序带来一定的侵入性,如果一个类被其他类继承,当这个类需要被修改的时候,必须要考虑到所有的子类,因为父类被修改之后,所有涉及到子类的功能,都有可能发生故障。所以继承会降低一定的可移植性,增加对象间的耦合性。所以需要遵守里氏替换原则,只有扩展类才能替换基类,才不会影响软件正常使用,也能保证父类的复用性,同时也能降低系统出错率,这样版本升级时才能保证很好的版本兼容性。
父类引用指向子类对象称为向上转型,其实就是箱子是父类,箱子里的内容是子类。
1
Animal a = new Cat(); // 向上转型
- 多态 : 同一个行为具有多个不同表现形式或形态的能力,其实就是子类重写父类的方法,再用向上转型的方式调用一下子类的这个方法,例如父类是打印机,子类是彩色打印机,子类重写一下打印方法,并将内容换成有颜色的打印,最后在Main调用一下即可,这样看起来就是父类又可以黑白打印,又可以彩印,具有多形态的打印了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Printer {
public void print(String content) {
System.out.println(content); // 打印内容
}
}
class ColorPrinter extends Printer {
@Override
public void print(String content) {
System.out.println("\033[31;4m" + content + "\033[0m"); // 带有颜色打印内容
}
}
public class Main {
public static void main(String[] args) {
Printer p = new ColorPrinter(); // 向上转型
p.print(); // 彩印
}
}
多态分为两种情况:编译时多态和运行时多态。如果在编译时能够确定执行多态方法中的哪一个,称为编译时多态,否则称为运行时多态。
运行时多态 : 也叫动态绑定,在执行期间判断引用对象的实际类型,根据实际类型判断并调用相应的属性和方法。运行时多态有三个条件,继承、覆盖(重写)、向上转型。也就是上面展示的样例内容。
数据类型
装箱拆箱和缓存池
8个基本类型boolean/1、byte/8、char/16、short/16、int/32、float/32、long/64、double/64,每个基本数据类型都对应了一个包装类型。主要说装箱拆箱,最快速的理解就是,如果把数据比作苹果,那么箱子里就有各种削皮器、小刀等等,装箱就是把苹果放到这个箱子里,别人就可以吃到削皮后的苹果或者切块后的苹果。拆箱就是直接吃苹果,或者把苹果从箱子里拿出来。也就是带有可调用方法的就是包装类型,例如Integer,可以调用valueOf(int)、parseInt()等方法,那么不带有的就是基础类型, 例如int。
包装类型的缓存机制,也就是缓存池,用于在应用程序中存储和管理临时对象或数据的内存池。主要用于减少对象的创建和销毁,池化技术的目的都差不多,像是对象池、线程池等等,都是为了对象的复用。
用Integer举例,在Java8中,Integer缓存池的大小默认为-128~127。若缓存池没有满,则会使用valueOf()判断,值在直接返回,不在则创建。若缓存池满了,则使用一些策略来处理新的缓存请求,例如LRU算法、LFU算法、FIFO、TTL等等。
1
2
3
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
反射
Java的反射机制是指Java语言提供的一种功能,允许在运行时动态地访问和操作类、方法、字段和构造函数等信息。通过反射,程序可以在运行时探测类的结构,并动态地调用对象的方法或访问字段。反射机制为开发者提供了极大的灵活性,但也有可能带来性能开销和安全风险。 原理是基于类加载机制,类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。
主要功能的使用方法示例 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取类信息 : 通过 Class 类获取类的信息,可以是任何对象的 Class 对象,也可以通过类字面量(如 String.class)或类加载器获取。
Class<?> clazz = Class.forName("java.lang.String");
// 获取构造函数和创建实例 : 使用 Class 对象的 getConstructor 或 getDeclaredConstructor 方法可以获取构造函数的信息。
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance("Hello");
// 访问字段 : 使用 Class 对象的 getField 或 getDeclaredField 方法可以获取字段的信息。
Field field = clazz.getField("value");
Object value = field.get(instance);
// 访问方法 : 使用 Class 对象的 getMethod 或 getDeclaredMethod 方法可以获取方法的信息。
Method method = clazz.getMethod("length");
int length = (Integer) method.invoke(instance);
// 访问类的信息 : 通过 Class 对象可以获取类的全名、包名等信息。
String className = clazz.getName();
// 修改类的结构 : 通过反射,可以访问和修改类的私有字段和方法。
Field privateField = clazz.getDeclaredField("somePrivateField");
privateField.setAccessible(true);
privateField.set(instance, "New Value");
使用场景 :
- 框架和库 : 许多Java框架使用反射来实现依赖注入、对象关系映射等功能。
1
2
3
4
5
// 例如 Spring 框架使用反射来实现依赖注入(DI)和面向切面编程(AOP)。在运行时,Spring 会扫描类和配置文件,利用反射动态创建和管理对象,并将它们注入到需要的地方。
// Spring自动注入
@Autowired
private MyService myService;
- 动态代理 : Java 动态代理(java.lang.reflect.Proxy)依赖反射机制创建运行时代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Java 动态代理允许在运行时创建接口的实现代理,使用反射机制动态地处理方法调用。这在实现远程方法调用(RMI)、日志记录、性能监控等场景中非常有用。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface MyInterface {
void doSomething();
}
class MyInvocationHandler implements InvocationHandler {
private final MyInterface original;
MyInvocationHandler(MyInterface original) {
this.original = original;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call");
Object result = method.invoke(original, args);
System.out.println("After method call");
return result;
}
}
public class Main {
public static void main(String[] args) {
MyInterface original = new MyInterfaceImpl();
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[]{MyInterface.class},
new MyInvocationHandler(original)
);
proxy.doSomething();
}
}
- 序列化和反序列化 : 反射可以用于在运行时处理对象的序列化和反序列化。
1
2
3
4
5
6
7
8
9
// 例如 Java 序列化过程将对象转换为字节流并存储或传输。反射用于读取对象的字段并将其写入字节流,反序列化时则从字节流中恢复对象。
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(myObject);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"));
MyObject myObject = (MyObject) ois.readObject();
ois.close();
- 开发工具 : IDE 和测试工具(如 JUnit)使用反射来检测类和方法,动态生成代码或执行测试。
1
2
3
4
5
6
7
8
9
// 代码生成工具可以使用反射生成 Java 类、接口或方法。例如,使用反射生成与数据库表对应的 Java 类。
StringBuilder sb = new StringBuilder();
sb.append("public class ").append(className).append(" {\n");
for (Field field : fields) {
sb.append(" private ").append(field.getType().getName()).append(" ").append(field.getName()).append(";\n");
}
sb.append("}");
String generatedClass = sb.toString();
异常
Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:
受检异常 : 需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复。
非受检异常 : 是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。
最佳实践 :
- 捕获特定异常:尽量捕获最具体的异常类型,以便进行有针对性的处理。
- 不要捕获
Throwable
:捕获Throwable
会捕获所有错误和异常,不建议使用。 - 记录异常信息:使用日志记录异常信息,帮助调试和跟踪问题。
- 合理使用自定义异常:创建自定义异常类时,确保提供有用的错误信息。
泛型
泛型的本质是为了参数化类型,类似于形参,调用时传参才知道具体的类型是什么。适用于多种数据类型执行相同的代码的场景。原理是伪泛型,也就是在编译阶段会进行类型擦除Type Erasure,将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型)。主要了解以下几个方面的使用,泛型类、泛型接口、泛型方法、泛型的上下限和泛型通配符。
- 泛型类 : 使用类型参数的类
1
2
3
4
5
// 定义一个泛型类
public class Box<T>
// 使用泛型类
Box<Integer> integerBox = new Box<>();
- 泛型接口 : 允许在接口中使用类型参数,使用时指定具体的类型。
1
2
3
4
5
6
7
8
9
// 定义一个泛型接口
public interface Comparable<T> {
int compareTo(T o);
}
// 实现泛型接口
public class Person implements Comparable<Person>{
// ...
}
- 泛型方法 : 指方法定义时可以使用类型参数,这些参数可以在方法体内使用。泛型方法不一定要放在泛型类中。
1
2
3
4
5
6
// 定义一个泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
- 泛型的边界/上下限 : 有时需要限制泛型类型参数的范围,可以使用泛型边界来实现。这可以通过 extends 关键字来实现类型的上限边界。
1
2
3
4
5
6
7
8
9
10
11
// 定义一个带有上限边界的泛型方法
public class NumberUtils {
public static <T extends Number> void printNumber(T number) {
System.out.println(number);
}
public static void main(String[] args) {
printNumber(123); // 输出:123
printNumber(45.67); // 输出:45.67
}
}
- 泛型通配符 : 通配符用于表示任意类型。Java 泛型支持三种主要类型的通配符:
- 无界通配符 (<?>):表示任何类型。
- 上界通配符 (<? extends T>):表示 T 类型及其子类型。
- 下界通配符 (<? super T>):表示 T 类型及其父类型。
注解
Java的注解机制是Java5引入的一种强大的语言特性,它允许开发者在代码中添加元数据,以便在编译、类加载和运行时进行处理。注解机制主要用于提供额外的信息给编译器、运行时环境或框架,支持代码分析、生成和处理。
- 注解的基本概念
- 注解:注解是通过 @ 符号定义的特殊类型的修饰符。它们与类、方法、字段、参数等 Java 语言元素相关联,用于提供额外的信息。
- 定义注解:注解的定义以 @interface 关键字开始。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义注解
public @interface MyAnnotation {
String value() default "defaultValue"; // 注解的属性,提供默认值
int count() default 0;
}
// 使用注解
@MyAnnotation(value = "example", count = 10)
public class MyClass {
@MyAnnotation("methodAnnotation")
public void myMethod(@MyAnnotation("paramAnnotation") String param) {
// 方法体
}
}
- 内置注解/常用注解
- @Override:标记方法覆盖父类方法。
- @Deprecated:标记已过时的元素。
- @SuppressWarnings:抑制编译器的警告信息。
- 元注解 : Java 的元注解主要有以下四种
- @Retention : 元注解用于指定注解的保留策略,即注解在什么阶段可用。
1
2
3
4
5
/*
RetentionPolicy.SOURCE : 源代码中保留,编译后丢失。
RetentionPolicy.CLASS : 编译时保留在.class文件中,但在运行时不可用。
RetentionPolicy.RUNTIME : 运行时仍然可用,可以通过反射读取。
*/
- @Target 元注解用于指定注解可以应用的 Java 元素类型,例如类、方法、字段等。
1
2
3
4
5
6
7
8
9
10
/*
ElementType.TYPE:可以应用于类、接口、枚举、注解等。
ElementType.FIELD:可以应用于字段(成员变量)。
ElementType.METHOD:可以应用于方法。
ElementType.PARAMETER:可以应用于方法参数。
ElementType.CONSTRUCTOR:可以应用于构造函数。
ElementType.LOCAL_VARIABLE:可以应用于局部变量。
ElementType.ANNOTATION_TYPE:可以应用于注解类型。
ElementType.PACKAGE:可以应用于包声明。
*/
- @Inherited 元注解用于指示某个注解类型是否可以被继承。当一个类被标记为某个注解,并且这个注解有 @Inherited 元注解时,这个注解会被其子类继承。
- @Documented 元注解用于指定被注解标记的注解类型应当包含在 Javadoc 文档中。被 @Documented 注解标记的注解,会被包含在生成的文档中,以便开发者能够在文档中查看注解信息。
- 注解的用途
- 代码生成:框架和工具可以利用注解生成代码或配置文件。
- 框架配置:许多框架(如 Spring、Hibernate)使用注解来配置行为。
- 文档生成:工具可以根据注解生成文档或其他相关资源。
- 编译时检查:注解可以帮助编译器进行额外的检查和验证。
SPI机制
Java 的 SPI(Service Provider Interface)机制是一种服务提供者接口机制,允许在运行时动态地发现和加载服务实现。这一机制主要用于实现模块化和插件化架构,使得系统能够在运行时加载不同的实现而不需要修改代码。
SPI 机制的基本概念
服务接口:SPI 机制基于服务接口,这些接口定义了服务的行为。接口通常是由服务的消费者定义的,服务提供者则实现这些接口。
服务提供者:服务提供者实现了服务接口,并提供具体的服务实现。服务提供者可以是多个,系统可以根据需求加载不同的服务实现。
服务配置文件:服务提供者在
META-INF/services
目录下创建配置文件。配置文件的名称是服务接口的完全限定名,内容是服务提供者的完全限定名。服务加载:系统通过
ServiceLoader
类动态加载服务实现。ServiceLoader
负责读取配置文件,查找并实例化服务提供者。
SPI 机制的工作流程
定义服务接口:首先,定义一个服务接口,该接口描述了服务提供者应实现的行为。
示例:
1 2 3
public interface PaymentService { void processPayment(double amount); }
实现服务接口:然后,创建一个或多个服务提供者实现服务接口。
示例:
1 2 3 4 5 6
public class CreditCardPaymentService implements PaymentService { @Override public void processPayment(double amount) { System.out.println("Processing credit card payment of $" + amount); } }
配置服务提供者:在
META-INF/services
目录下创建一个文件,文件名是服务接口的完全限定名,文件内容是服务提供者的完全限定名。- 配置文件路径:
META-INF/services/com.example.PaymentService
配置文件内容:
1
com.example.CreditCardPaymentService
- 配置文件路径:
加载服务提供者:使用
ServiceLoader
类在运行时动态加载服务提供者。示例:
1 2 3 4 5 6 7 8 9 10 11
import java.util.ServiceLoader; public class PaymentServiceLoader { public static void main(String[] args) { ServiceLoader<PaymentService> loader = ServiceLoader.load(PaymentService.class); for (PaymentService service : loader) { service.processPayment(100.0); } } }
SPI 机制的特点
模块化:SPI 机制支持将功能模块化,允许在运行时添加或替换功能模块,而无需修改主程序代码。
灵活性:系统可以根据配置文件动态选择不同的服务实现,实现了系统的灵活扩展。
解耦合:通过接口和配置文件的方式,服务提供者与服务消费者之间的耦合度降低,系统更加灵活和可维护。
使用场景
- 插件系统:通过 SPI 实现插件化架构,允许第三方插件与主程序动态集成。
- 模块化框架:在模块化框架中使用 SPI 机制,以支持不同模块之间的扩展和替换。
- 实现抽象工厂模式:SPI 可以用于创建和管理不同的服务实现,类似于抽象工厂模式的应用场景。
总结
特性 | 说明 | 原理 | 实现方式 |
---|---|---|---|
封装 | 包装数据 | 变量private和getter、setter | |
继承 | 子类继承父类 | 需要遵守里氏替换原则 | 关键字extends |
多态 | 同一个行为具有多个不同表现形式 | 子类重写父类的方法后再用向上转型的方式调用一下子类的这个方法 | Printer p = new ColorPrinter(); |
装箱拆箱 | 使基本数据类型带有方法 | 缓存池 | Integer.valueOf(int) |
反射 | 运行时动态地访问和操作类、方法、字段和构造函数 | 类加载机制 | Class 和 java.lang.reflect 一起对反射提供了支持 |
泛型 | 多种数据类型执行相同的代码的场景 | 伪泛型,在编译阶段会进行类型擦除Type Erasure | 泛型类、泛型接口、泛型方法、泛型的上下限和泛型通配符 |
注解 | 允许开发者在代码中添加元数据,以便在编译、类加载和运行时进行处理 | 反射机制 | 自定义注解可用@interface关键字 |
SPI机制 | 启用框架扩展和替换组件 | 有点类似IOC的思想,将装配的控制权移到程序之外 | ServiceLoader |