- Java 是一种高级编程语言和计算平台。它最初由Sun(后被Oracle收购)于1995年发布,旨在实现“一次编写,到处运行”的理念。
- Java SE(标准版):是 Java 平台的基本版本,也是最基本的 Java 编程平台。提供了 Java 编程语言的核心功能,包括基本的语言结构、标准库、输入/输出、多线程支持等。Java SE 适用于通用的桌面应用程序、命令行工具、小型服务、移动应用程序等各种领域。
- Java EE(企业版):是在 Java SE 的基础上构建的,专门用于开发和运行企业级应用程序的平台。它提供了一组扩展和 API,用于构建大型、分布式、可伸缩、高性能的应用程序,如企业级 Web 应用、电子商务系统等。Java EE 包括了 Servlet、JSP、EJB、JPA等各种技术和规范,以支持不同类型的企业级应用。

Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的高级程序设计语言。Java 可运行于多个平台,如 Windows, Mac OS 及其他多种 UNIX 版本的系统。
移动操作系统 Android 大部分的代码采用 Java 编程语言编程。
面向对象 三大特性
封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
继承
继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型。
1 | // 向上继承是面向对象编程中的一个关键概念,它通过创建一个统一的接口和通用的父类,支持多态性,减少代码重复,提高 |
多态
编译时多态主要指方法的重载(一个类中的多个同名方法)
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定(父,子类之间);有三个条件:继承、覆盖(重写)、向上转型
1 | public class Animal { public void play() { System.out.println("aoaoao.."); }} |
数据类型
数据类型定义了数据的种类和取值范围,以及对这些数据执行的操作。在Java中,数据类型主要分为两类:
1. 基本数据类型
- 基本数据类型:这些数据类型是Java语言的一部分,用于表示基本的数据值。有以下几种:
整数类型:byte、short、int、long; 浮点数类型:float、double; 字符类型:char; 布尔类型:booleanInteger.MAX_VALUE
常量表示int类型的最大值,值为2147483647。类型 byte short int long float double char boolean 大小(Byte) 1 2 4 8 4 8 2 1 - 包装类型:包装是指将基本数据类型包装在对象中的过程。这是因为 Java 是面向对象的编程语言,需要将基本数据类型封装在对象中,从而在面向对象的上下文中使用它们,在处理集合、泛型、反射等Java编程中的许多场景中非常有用。例如,你可以将一个 int 存储在一个 List<Integer> 中,而不是 List<int>
类型对应:byte - Byte, short - Short, int - Integer, long - Long, float - Float, double - Double, char - Character, boolean - Boolean1
2Integer x = 2; // 装箱,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
int y = x; // 拆箱 - 类型转换(char、String、int和double之间)
1
2
3
4// int 转 double: int i = 123;
double d = (double)i; // 或者直接 d = i;
// double 转 int: double d = 123.45;
int i = (int)d; // 使用类型转换,但请注意这会截断小数部分。结果是 1231
2
3
4
5
6// int 转 char:int i = 97;
char c = (char)i; // 通过 ASCII值转换。97 对应的字符是 'a'
c = (char)(c + 32); // 大小写字母差值32,与cpp中的一样
// char 转 int:char c = 'a';
int i = (int)c; // 通过ASCII值转换。'a' 的ASCII值是 97
int j = '9' - '0';1
2
3
4
5
6
7
8// int 转 String
int number = 123;
String strNumber1 = String.valueOf(number);
String strNumber2 = "" + number;
String strNumber3 = Integer.toString(number);
// String 转 int
int number = Integer.parseInt(str); // 默认十进制
int numBinary = Integer.parseInt(binaryStr, 2); // 二进制1
2
3
4// String 转 double: String doubleStr = "123.45";
double d = Double.parseDouble(doubleStr);
// double 转 String: double d = 123.45;
String str = String.valueOf(d); // 或者 "str = d +;"1
2
3
4
5// char 转 String:char c = 'a';
String str = Character.toString(c);
str += c; // 将 `char` 直接与字符串连接(`char` 会自动被转换成 `String`)。
// String 转 char:String str = "hello";
char c = Character.charAt(str, 0); // 获取字符串的第一个字符
2. 引用数据类型
引用数据类型用于引用对象,而不是直接存储数据值。引用数据类型包括类、接口、数组、枚举等。
- 引用:引用是一个变量,用于存储对象的内存地址。它指向对象在堆内存中的位置,而不是对象本身。通过引用,可以访问和操作对象的数据和方法。
- 引用类型:引用类型是Java中的数据类型之一,用于声明引用变量。Java的引用类型包括类、接口、数组、枚举和注解。引用类型的变量可以用来引用相应类型的对象。
- 堆内存:在Java中,所有的对象都存储在堆内存中。引用变量存储的是对象在堆内存中的地址。当创建一个新对象时,它被分配到堆内存中,然后引用变量被赋予该对象的地址。
- 栈内存:Java中的引用变量本身存储在栈内存中。栈内存用于存储局部变量,包括引用变量。引用变量在栈上分配内存,但它们指向的对象存储在堆内存中。
- 强引用:强引用是最常见的引用类型。当一个对象被强引用变量引用时,它不会被垃圾回收器回收,直到引用变量不再引用该对象。如果没有任何强引用指向一个对象,该对象就会成为垃圾,可以被垃圾回收。
- 软引用:软引用用于描述一些还有用但并非必需的对象。当内存不足时,垃圾回收器可能会回收被软引用引用的对象,释放内存。这可以防止内存溢出,但不会保证垃圾回收器什么时候回收这些对象。
- 弱引用:弱引用用于描述非必需对象的引用。弱引用的对象会在垃圾回收器下次运行时被回收,不论内存是否足够。这使得弱引用适用于临时对象和缓存。
- 虚引用:虚引用是最弱的引用类型。虚引用的对象没有直接访问的权限,它主要用于跟踪对象是否被回收。虚引用必须和引用队列(ReferenceQueue)一起使用,以便在对象被回收时收到通知。
主要引用类型:
- 类引用类型:类引用类型用于引用类的实例,即对象。这是Java中最常见的引用类型。例如,如果有一个类
Person
,您可以创建Person
类型的引用来引用不同的Person
对象:Person person = new Person();
- 接口引用类型:接口引用类型用于引用实现了特定接口的对象。例如,如果有一个接口
Drawable
,您可以创建Drawable
类型的引用来引用实现了Drawable
接口的对象:Drawable drawable = new Circle();
- 数组引用类型:数组引用类型用于引用数组对象。数组可以包含基本数据类型或引用类型的元素。例如,
int[] numbers
是一个引用类型,用于引用整数数组。 - 枚举引用类型
- 泛型引用类型
- 父类引用类型:父类引用类型用于引用子类对象。这可以用于实现多态。例如,如果有一个父类
Animal
和一个子类Dog
,您可以创建Animal
类型的引用来引用Dog
对象:Animal animal = new Dog();
Java中的引用
不是直接的内存地址,而是对实际对象的间接引用。这使得Java程序员不需要关心内存管理,因为Java虚拟机(JVM)会自动管理对象的创建和销毁。
与C++中的指针不同。Java中的引用是一种高级抽象,它隐藏了对象的底层内存地址和操作,使得程序员不需要关心内存管理。
Java中的引用本质上是一个抽象的句柄(handle),它不直接指向对象的内存地址,而是指向Java虚拟机(JVM)内部的数据结构,该数据结构包含了对象的信息以及对象在堆内存中的位置。这种抽象层级使Java更安全,因为程序员无法直接操纵内存地址,从而避免了许多常见的内存错误,如指针溢出和内存泄漏。
枚举类
Java中的枚举类是一种特殊的类,用于定义一组常量。枚举类可以包含字段、方法和构造函数。
- 定义简单的枚举类:
1
2
3
4
5
6
7
8
9
10
11// 定义一个简单的枚举类
enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
public class EnumExample {
public static void main(String[] args) {
// 使用枚举常量
Day today = Day.MONDAY;
System.out.println("Today is: " + today);
}
} - 枚举类包含字段和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 定义包含字段和方法的枚举类
enum Color {
RED("#FF0000"), GREEN("#00FF00"), BLUE("#0000FF");
private String hexCode;
// 构造函数
private Color(String hexCode) {
this.hexCode = hexCode;
}
// 获取颜色的十六进制代码
public String getHexCode() {
return hexCode;
}
}
public class EnumWithFieldsAndMethods {
public static void main(String[] args) {
// 使用枚举常量和方法
Color color = Color.BLUE;
System.out.println("Color is " + color + " with hex code " + color.getHexCode());
}
} - Java中的枚举类可以包含抽象方法。在这种情况下,每个枚举常量都必须实现抽象方法。
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// 定义带有抽象方法的枚举类
enum Operation {
ADD {
public int apply(int x, int y) {
return x + y;
}
},
SUBTRACT {
public int apply(int x, int y) {
return x - y;
}
},
// 定义抽象方法
public abstract int apply(int x, int y);
}
public class EnumWithAbstractMethod {
public static void main(String[] args) {
// 使用带有抽象方法的枚举
int result = Operation.ADD.apply(5, 3);
System.out.println("Result: " + result);
}
}
String
String 被声明为 final,因此它不可被继承。内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String类 不可变。
- String Pool :如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。
- String 不可变,StringBuffer 和 StringBuilder 可变。
String 不可变,因此是线程安全的,StringBuilder 不是线程安全的,StringBuffer 是线程安全的,内部使用 synchronized 进行同步1
2
3
4
5
6
7
8String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false; s1 和 s2 采用 new String() 的方式新建了两个不同对象
String s3 = s1.intern();
System.out.println(s1.intern() == s3); // true; s3 是通过 s1.intern() 方法 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象
String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5); // true; 采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中
Java 容器
- 容器:Java 容器就是可以容纳其他 Java 对象的对象。Java Collections Framework(JCF) 为开发者提供了通用的容器,始于JDK 1.2
- 优点: 降低编程难度,提高程序性能,提高API间的互操作性,降低设计和实现相关API的难度,增加程序的重用性
- 对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程。
- 容器接口 主要包括 Collection(容器类) 和 Map 两种,
Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
1. Collection
Set
主要功能是保证存储的集合不会重复,至于集体是有序还是无序的,需要看具体的实现类,比如 TreeSet 有序,HashSet 无序
- TreeSet: 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
List
- ArrayList:基于动态数组实现,支持随机访问。
- Vector:和 ArrayList 类似,但它是线程安全的。
- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
Queue
队列,有序,严格遵守先进先出。
- LinkedList:可以用它来实现双向队列。实现了 Queue 接口。
- PriorityQueue:基于堆结构实现,可以用它来实现优先队列???内部是基于数组构建的,用法就是你自定义一个 comparator ,自己定义对比规则,这个队列就是按这个规则来排列出队的优先级。
2. Map
存储的是键值对,也就是给对象(value)搞了一个 key,这样通过 key 可以找到那个 value。
- TreeMap:基于红黑树实现。有序。
- HashMap:基于哈希表实现。
- HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
实现类
ArrayList
1 | add(E e) // 向列表末尾添加元素 |
LinkedList
1 | add(E e) // 在列表末尾添加元素 |
Stack & Queue
1 | push(E e) // 将元素压入栈顶 |
1 | offer(E e) // 将元素添加到队列尾部 |
PriorityQueue
1 | add(E e) // 向队列中添加元素 |
HashSet & HashMap
1 | add(E e) // 向集合中添加元素 |
1 | put(K key, V value) // 将键值对存入 Map |
LinkedHashSet&Map
TreeSet & TreeMap
TreeSet
add(E e): 向集合中添加元素
contains(Object o): 判断集合是否包含指定元素
remove(Object o): 移除集合中指定的元素
TreeMap
put(K key, V value): 将键值对存入 Map
get(Object key): 获取指定键的值
containsKey(Object key): 判断 Map 是否包含指定键
remove(Object key): 移除 Map 中指定键的键值对
WeakHashMap
数据存储
在Java中,数据的存储方式涉及到内存的不同区域,主要包括堆内存和栈内存。
- 栈内存(Stack Memory):
- 存储特点:栈内存用于存储局部变量(方法内部定义的变量)和方法调用的执行上下文(包括方法的参数、返回地址等信息)。每个线程都有自己的栈内存,用于管理方法调用和局部变量的生命周期。
- 生命周期:局部变量的生命周期与方法的执行周期相关联。当方法被调用时,会在栈内存中为局部变量分配内存,当方法执行完毕时,栈内存会自动释放局部变量的内存。
- 线程安全:栈内存是线程私有的,因此它是线程安全的。不同线程的栈内存互相独立,不会发生竞态条件。
- 堆内存(Heap Memory):
- 存储特点:堆内存用于存储对象和数组等引用数据类型。是所有线程共享的内存区域,用于管理动态分配的对象的生命周期。
- 生命周期:对象在堆内存中的生命周期不受方法调用的限制。对象在被创建时分配内存,在不再被引用时会被垃圾回收器自动回收。
- 线程安全:由于堆内存是共享的,需要特别注意多线程访问的同步问题。Java提供了一些机制来确保堆内存中的数据安全,如 synchronized关键字和 java.util.concurrent 包中的工具类。
- 数据存储示例:
- 当创建一个对象时,对象的引用存储在栈内存中,而对象的实际数据存储在堆内存中。例如,Person person = new Person();
- 在堆内存中,会为Person对象分配内存空间,这个对象包含了Person类中定义的所有成员变量(属性)。例如,如果Person类有一个名为name的成员变量,那么堆内存中的Person对象将包含一个用于存储name属性值的内存位置。堆内存中的对象是独立的,每个new Person()创建一个新的对象,其数据在堆内存中不会重叠。
- 在栈内存中,会创建一个名为person的引用变量,这个引用变量存储了指向堆内存中Person对象的地址。也就是说,栈内存中的person变量实际上存储了对堆内存中Person对象的引用。当你访问person时,实际上是通过栈内存中的引用找到堆内存中的对象,然后可以操作对象的属性和方法。
- 局部变量(例如方法中的局部变量)的值和引用都存储在栈内存中。
- 数组的引用存储在栈内存中,而数组的元素(对象引用或基本数据类型)存储在堆内存中。
- 静态变量(
static
关键字修饰的变量)存储在方法区(在JVM规范中称为永久代)。
- 当创建一个对象时,对象的引用存储在栈内存中,而对象的实际数据存储在堆内存中。例如,Person person = new Person();
- 缓存池
1
2
3
4
5
6
7
8
9
10Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false; new Integer() 每次都会新建一个对象
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true; valueOf()会使用缓存池中的对象,多次调用会取得同一个对象的引用
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true; 编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf()
// 因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
运算
参数传递
在Java中,方法参数的传递方式是按值传递,而不是引用传递。这意味着当你将参数传递给方法时,实际上是将参数的值传递给了方法,而不是参数本身。这包括基本数据类型和对象引用。
1、基本数据类型: 作为参数传递的是该数据的副本。任何在方法内部对参数值的修改都不会影响到原始数据。
2、对象引用: 对象引用也是按值传递的,但需要理解的是对象引用指的是对象在内存中的地址(间接引用,非直接地址)。当你将一个对象作为参数传递给方法时,传递的是对象引用的副本,而不是对象本身。这意味着在方法内部可以修改对象的状态,但不能改变对象引用指向的内存地址。
隐式类型转换
- “2.5” 字面量属于 double 类型,不能直接将 2.5 直接赋值给 float 变量,因为Java 不能隐式执行向下转型,这会使得精度降低。
1
float f = 2.5f; // 错误写法:float f = 2.5;
- 字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地 下转型 。但是使用 += 运算符可以执行隐式类型转换。
1
2short s1 = 1; // 不能 s1 = s1 + 1;
s1 += 1; // 相当于 s1 = (short) (s1 + 1);
有/无符号数,原/反/补码
在 Java 中,所有的整数类型都是有符号的。Java中并没有内置的无符号整数类型;如果真的需要,可以使用大整数类 BigInteger
。
无符号整数通常用于表示大范围正整数值,以及位运算和处理原始数据。它们可以扩展表示的范围,因为不需要一位来表示符号。
原码是最简单的表示形式,其中最高位表示符号(0正数,1负数),其余位表示数值的大小。例如,+3原码为0011,-3原码为1011。
反码是对原码的符号位以外的位取反得到的结果。正数的反码与其原码相同,负数的反码是其原码除符号位外取反。
补码是计算机中最常用的表示方式。补码的计算方法是对反码加1。正数的补码与其原码相同,负数的补码是其反码加1。
在Java中,整数类型(如byte、short、int、long)使用补码来表示有符号整数。这种表示方式使得负数的加法和减法运算可以与正数的运算使用同一套算法来执行,简化了计算机中的运算。
位运算
- 在 Java 中,位运算是操作二进制位的一种操作,它们可以用来执行诸如位移、与、或、非等操作。
1
2
3
4
5
6
7
8int a = 5, b = 3; // 二进制表示:0101, 0011
int andResult = a & b; // 0101 & 0011 = 0001,结果为 1
int orResult = a | b; // 0101 | 0011 = 0111,结果为 7
int xorResult = a ^ b; // 0101 ^ 0011 = 0110,结果为 6
int complementA = ~a; // ~0101 = 1010(补码表示),结果为 -6(-6的八位补码就是1111 1010)
int leftShift = a << 1; // 0101 左移 1 位 = 1010,结果为 10
int rightShift = a >> 1; // 0101 右移 1 位 = 0010,结果为 2(使用符号位填充空位)
int unsignedRightShift = a >>> 1; // 0101 无符号右移 1 位 = 0010,结果为 2(使用 0 填充空位) - 在 Java 中,十六进制表示使用前缀
0x
或者0X
来标识一个十六进制数。
使用位运算符时:Java 内部会将这些十六进制数转换为对应的二进制形式,然后执行位运算操作,最后转换回十六进制的结果。1
2
3
4
5int hex1 = 0xA, hex2 = 0x7; // 十六进制数 A 对应十进制数 10, 十六进制数 7 对应十进制数 7
int andResult = hex1 & hex2; // 十六进制数 A & 7 = 1010 & 0111 = 0010,结果为 2
int complementHex1 = ~hex1; // ~1010 = 0101(补码表示),结果为 -11
int rightShift = hex1 >> 1; // 十六进制数 A 右移 1 位 = 1010 右移 1 位 = 0101,结果为 5
int unsignedRightShift = hex1 >>> 1; // A 无符号右移 1 位 = 1010 无符号右移 1 位 = 0101,结果为 5 n & (n−1)
,把 n 的二进制位中的最低位的 1 变为 0。- 颠倒给定的 32 位无符号整数 n 的二进制位
1
2
3
4for(int i = 0; i < 32 && n != 0; i++) {
ans = ans | (n & 1) << (31 - i);
n >>>= 1;
}
类与对象
- 类(Class): 类是对象的模板,它定义了对象的属性(成员变量)和行为(方法)。类是一种抽象的概念,描述了一类事物的共同特征。在Java中,类是一种引用数据类型。
- 对象(Object): 对象是类的实例,是具体的、实际存在的数据。每个对象都有自己的状态(成员变量的值)和行为(方法的调用)
创建
- 创建对象实例: 使用关键字
new
可以创建一个类的对象。例如:1
MyClass myObject = new MyClass();
- 通过反射: 使用 Java 的反射机制可以在运行时获取类的信息,创建对象实例。例如:
1
2Class<?> clazz = Class.forName("com.example.MyClass");
MyClass myObject = (MyClass) clazz.newInstance(); - 通过工厂方法: 有时候,对象的创建可能通过工厂方法,例如静态方法等来实现。
- 通过new创建实例对象demo,放在堆内存上;然后再main方法的栈帧中存放一个局部变量,存放了demo对象在堆上的地址,即建立起一个引用关系。将局部变量置null后,引用就不存在了,堆上的实例对象demo就可以回收了。
使用
1 | // 一旦对象被创建,可以通过其引用调用其属性和方法 |
回收
对象的回收: Java虚拟机通过垃圾回收器(Garbage Collector)来自动回收不再被引用的对象。垃圾回收器会定期检查程序中的对象,识别哪些对象没有被任何引用指向,然后释放它们占用的内存。这个过程是自动的,开发者无需手动管理大部分对象的内存。
在某些情况下,可以通过手动设置对象引用为null
来提示垃圾回收器回收对象。但这通常不是必要的,因为现代的垃圾回收器在大多数情况下能够很好地管理内存。
对象的生命周期从创建开始,直到没有任何引用指向它时结束。垃圾回收器负责在对象不再被引用时将其销毁。类的回收: 在Java中,类的回收通常不是显式的操作。类加载和卸载是由类加载器(ClassLoader)来管理的。当一个类不再被任何对象引用,并且ClassLoader不再需要这个类时,该类可能会被卸载。在标准的Java应用程序中,类的卸载通常不是很常见,因为大多数类在整个应用程序的生命周期内都是可见的。
需要注意的是,类的卸载是Java虚拟机实现的一个可选特性,不是所有的Java虚拟机都支持类的卸载。类的卸载通常发生在特定的环境中,比如一些动态生成和卸载类的场景。
总体而言,Java虚拟机通过垃圾回收器自动管理对象的内存,而类的加载和卸载通常由类加载器来处理。这种自动化的内存管理减轻了开发者的负担,使得Java程序更容易编写和维护。
继承
访问权限
Java 中有三个访问权限修饰符: private、protected 以及 public。可以对类或类中的成员(字段以及方法)加上访问修饰符,如果不加访问修饰符,表示包级可见(default
,介于private和protected之间)。private
:私有访问修饰符。被 private 修饰的成员(字段、方法、内部类等)仅对定义它们的类可见。这意味着这些成员只能在同一个类内部访问,对于类的外部(其他类)是不可见的。protected
:受保护的访问修饰符。被 protected 修饰的成员对于定义它们的类、同一包内的其他类以及子类可见。在不同包中的类无法访问受保护成员。public
:公共访问修饰符。被 public 修饰的成员对于所有类都是可见的,无论是同一包内还是不同包中的类,都可以访问 public 成员。
extend 与 implement
extend
(扩展):使用于类之间的关系。用于创建类的子类(子类继承父类)。子类可以继承父类的属性和方法。 一个类只能extend
一个类,即Java是单继承的,但是可以通过接口实现来弥补这一不足。implement
(实现):使用于接口与类之间的关系。 用于让类实现一个或多个接口,implement
多个接口,实现多继承的效果。 类实现接口时,需要提供接口中定义的所有方法的具体实现。
抽象类与接口
抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。接口的字段默认都是 static 和 final 的。
super
- 访问父类的构造函数: 可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。
- 访问父类的成员: 如果子类重写了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。
1
2
3
4
5
6
7
8
9
10
11
12
13public class SuperExample {
protected int x, y;
public SuperExample(int x, int y) { this.x = x; this.y = y; }
public void func() { System.out.println("SuperExample.func()"); }
}
public class SuperExtendExample extends SuperExample {
private int z;
public SuperExtendExample(int x, int y, int z) { super(x, y); this.z = z; }
public void func() { super.func(); System.out.println("SuperExtendExample.func()"); }
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func(); // 结果:SuperExample.func() SuperExtendExample.func()
重载与重写
- 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。为了满足里式替换原则,重写有以下两个限制:子类方法的访问权限必须大于等于父类方法;子类方法的返回类型必须是父类方法返回类型或为其子类型。使用 @Override 注解,可以让编译器帮忙检查是否满足上面的两个限制条件。
常用方法
Java标准库包含多个类和函数,提供了许多基本的工具和功能,包括但不限于:
1、java.lang
- 包含Java的基础类,例如String
、Object
、System
等。这个包是默认导入的,不需要手动引入。
2、java.util
- 提供集合框架、日期时间工具、随机数生成器等。
3、java.io
- 包含用于进行输入和输出的类和接口,例如File
、InputStream
、OutputStream
等。
4、java.net
- 提供了网络相关的类,比如URL
、URLConnection
等。
5、java.math
- 包含用于数学计算的类,例如BigInteger
、BigDecimal
等。
6、java.nio
- 提供了新的I/O类,支持非阻塞I/O、内存映射文件等。
7、java.util.concurrent
- 包含用于并发编程的实用工具和框架。
8、java.awt
和 javax.swing
- 提供了GUI开发相关的类。
大部分情况下,这些库中的类和函数默认可用,无需手动引入。然而,有一些更特定或较为不常用的类可能需要显式地引入相应的包。
数据处理
1 | // 1. 保存两位小数;import java.text.DecimalFormat;(text在标准库中,通常不需要手动引入) |
Object 通用方法
在Java中,Object
是所有类的基类。它是Java类继承层次结构的根,因此每个Java类都直接或间接地继承自Object
类。Object
类位于java.lang
包中,这意味着它不需要显式导入就可以在Java程序中使用。
java.lang
包是java语言的核心,提供基础类,包括基本Object类、Class类、String类、基本类型的包装类、基本的数学类等。Object
类定义了一些基本的方法,这些方法可以被所有的Java对象继承和使用。其中一些重要的方法包括:1
2
3
4
5
6
7
8public final native Class<?> getClass() // 返回对象的运行时类(`Class`对象)。
public native int hashCode() // 返回对象的哈希码值。它通常与`equals`方法一起使用,以便在使用哈希表等数据结构时能够快速查找对象。
public boolean equals(Object obj) // 用于比较两个对象是否相等。默认情况下,它比较的是对象的内存地址,但可以在子类中重写以实现自定义的相等性比较。
protected native Object clone() throws CloneNotSupportedException // 用于创建对象的浅拷贝。需要注意的是,为了使对象可克隆,子类需要实现`Cloneable`接口并重写`clone`方法。
public String toString() // 返回对象的字符串表示。默认下,它返回一个由类名和对象的哈希码组成的字符串,但可以在子类中重写以提供更有意义的字符串表示。
public final native void notify() // 用于多线程同步的方法。
public final native void wait(long timeout) throws InterruptedException // 使线程等待直到被通知。
protected void finalize() throws Throwable {} // 在对象被垃圾回收之前调用的方法。
equals()
equals() 与 == 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
1 | Integer x = new Integer(1); |
hashCode()
返回对象的哈希码。默认实现是基于对象的内存地址生成哈希码。
在实际应用中,一般需要在类中重写该方法,以便相等的对象具有相同的哈希码,以提高哈希表等集合的性能。
clone()
- cloneable:clone() 是 Object 的 protected 方法而非 public,一个类不显式去重写,其它类就不能直接调用该类实例的 clone()。
- 浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。
- 深拷贝:拷贝对象和原始对象的引用类型引用不同对象。
- clone() 的替代方案:使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
异常
- Throwable 可以用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。
其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:- 受检异常 : 需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;
- 非受检异常 : 是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复
反射
Java反射是一种强大的编程技术,允许在运行时检查、探索和操作类、对象、字段、方法以及其他成员。反射使你可以动态地创建对象、调用方法、获取和设置字段值,以及执行其他与类和对象相关的操作。
- 每个类都有一个 Class 对象,包含与类有关的信息。当编译一个新类时会产生一个同名的
.class文件
,其内容保存着 Class 对象。 - 类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用
Class.forName("com.mysql.jdbc.Driver")
这种方式来控制类的加载,该方法会返回一个 Class 对象。 - 反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至编译时期该类的 .class 不存在也可以加载进来。
- Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:
- Field : 可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
- Method : 可以使用 invoke() 方法调用与 Method 对象关联的方法;
- Constructor : 可以用 Constructor 创建新的对象。
- Class类:Java反射的核心类是
java.lang.Class
。每个类都有一个与之相关的Class
对象,你可以使用这个Class
对象来获取关于类的信息。1
2
3
4
5
6
7
8
9
10
11// 获取类的`Class`对象
Class<?> myClass = MyClass.class; // 通过类名
Class<?> myClass = obj.getClass(); // 通过对象
Class<?> myClass = Class.forName("com.example.MyClass"); // 通过类的全名
// 获取有关类的信息
String className = myClass.getName(); // 获取类名
String packageName = myClass.getPackage().getName(); // 获取包名
Class<?> superClass = myClass.getSuperclass(); // 获取父类
Class<?>[] interfaces = myClass.getInterfaces(); // 获取实现的接口
Field[] fields = myClass.getDeclaredFields(); // 获取所有字段
Method[] methods = myClass.getDeclaredMethods(); // 获取所有方法 - 实例化对象:通过反射,你可以动态地创建对象。例如:
1
2
3
4
5
6
7
8
9
10
11Class<?> myClass = Class.forName("com.example.MyClass");
Object obj = myClass.newInstance(); // 创建对象,需要无参构造函数
// 使用反射来获取、设置对象的字段值
Field field = myClass.getDeclaredField("fieldName");
field.setAccessible(true); // 设置字段可访问
Object value = field.get(obj); // 获取字段值
field.set(obj, newValue); // 设置字段值
// 反射允许你调用对象的方法。例如:
Method method = myClass.getDeclaredMethod("methodName", param1Type, param2Type);
method.setAccessible(true); // 设置方法可访问
Object result = method.invoke(obj, arg1, arg2); // 调用方法 - 数组与泛型:通过反射,你可以创建数组、获取数组元素的类型信息,以及处理泛型信息。
- 代理:Java反射还可用于创建动态代理,这是一种强大的技术,允许你创建实现特定接口的代理类以在运行时拦截和处理方法调用。
- 限制:尽管反射提供了强大的能力,但也有一些限制和性能开销。使用时需要小心处理异常、性能、访问控制等方面的问题。
泛型
- 泛型是 Java 中的一个核心特性,有助于提高代码的可重用性和类型安全性。
- 泛型的主要目的是参数化类型,允许你在类、接口、方法中使用类型参数。
1
2
3
4
5
6
7
8public class Box<T> {
private T t; // T stands for "Type"
public void set(T t) { this.t = t; }
public T get() { return t; }
}
public class ChildBox<T> extends Box<T> {
// 子类继承父类的泛型类型参数
} - 泛型通配符:Java中有通配符类型,允许你在不知道具体类型的情况下使用泛型。通配符包括?符号。
当你使用通配符作为方法参数时,可能需要捕获通配符以使用它。这可以通过在方法参数中使用?来实现。1
2
3public void processList(List<?> list) {
for (Object obj : list) { // 处理每个元素 }
} - 泛型上下界:你可以使用通配符来定义类型的上下界。例如,<? extends Number>表示类型必须是Number或其子类。<? super Integer>表示类型必须是Integer或其父类。
- 类型擦除:Java泛型通过类型擦除来实现。这意味着在编译时,泛型类型信息会被擦除,而在运行时,Java虚拟机将使用原始类型。这可能会导致一些限制,例如无法创建泛型数组。
注解
- Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。https://pdai.tech/md/java/basic/java-basic-x-annotation.html
- Spring框架中的注解 本质上是一种元数据,用于为应用程序的组件(例如类、方法、字段等)提供附加的信息,以指导Spring容器在应用程序运行时如何处理这些组件。这些注解告诉Spring容器如何创建、初始化、配置和管理这些组件,以及它们之间的关系。
Stream 流
在Java中,流(Stream)是用于处理集合数据的一种新的抽象。Java的流操作主要分为两种:中间操作和终端操作。流的处理操作通常应用于集合类(如List、Set、Map等)。
Java的流操作利用了函数式编程的思想,提供了一种更简洁、灵活、可读性更强的处理方式。通过合理使用中间操作和终端操作,可以实现丰富的数据处理功能。流操作也广泛应用于Java集合、文件IO、网络编程等场景。
- 中间操作:
- 中间操作主要用于对数据进行处理和转换,产生一个新的流。中间操作不会触发实际的处理,只是在流上进行各种操作。
- 常见的中间操作包括
filter
(过滤)、map
(映射)、distinct
(去重)、sorted
(排序)、limit
(限制结果数量)等。1
2
3
4
5
6
7
8List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Alice");
names.stream()
.filter(name -> name.length() > 3)
.distinct()
.sorted()
.map(String::toUpperCase)
.forEach(System.out::println);
- 终端操作:
- 终端操作是流的最终处理步骤,触发流的遍历和数据处理。终端操作会返回一个非流的结果,例如集合、数组、某个值等。
- 常见的终端操作包括
collect
(将流元素收集到集合中)、reduce
(对元素进行归约操作)等。1
2
3
4
5
6
7
8List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Alice");
List<String> filteredNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());
??Lambda 表达式
学习 http://doc.junbo.top/pages/viewpage.action?pageId=10798971
Lambda 表达式提供了一种简洁、清晰的语法,允许以更为函数式的方式编写代码。
Lambda 的引入使得代码变得更加简洁,尤其在使用函数式接口(只有一个抽象方法的接口)时,可以直接传递 Lambda 表达式,而不再需要传递匿名内部类的实例。Lambda 表达式在处理集合、并发编程、事件处理等场景中广泛应用,它是 Java 8 引入的一个重要特性,使得 Java 语言更好地支持函数式编程风格。
- Lambda 表达式主要用于定义内联的、匿名的函数。它是一个函数式接口的实例,即只有一个抽象方法的接口。基本语法如下:
1
2
3
4
5
6
7
8
9
10
11// `parameters` 指定了 Lambda 表达式的参数,`->` 是 Lambda 操作符,
// `expression` 或 `{ statements; }` 指定了 Lambda 表达式的主体。
(parameters) -> expression
(parameters) -> { statements; }
// 1. 一个简单的例子,计算两个数的和:
(int a, int b) -> a + b
// 2. 遍历列表并打印每个元素:
List<String> list = Arrays.asList("apple", "banana", "orange");
list.forEach(s -> System.out.println(s));
// 3. 使用 `Runnable` 接口创建一个线程:
new Thread(() -> System.out.println("Hello, Lambda!")).start();
Java 动态代理机制 ??
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
- 静态代理
代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是实际对象。
优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
缺点:不同的接口要有不同的代理类实现,会很冗余 - JDK 动态代理
为了解决静态代理中,生成大量的代理类造成的冗余;JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,jdk的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象 jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口。
优点:解决了静态代理中冗余的代理实现类问题。
缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。 - CGLIB 代理
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
缺点:技术实现相对难理解些。
BIO、NIO 和 AIO 的区别
??
Java 设计模式
设计模式是为了解决软件开发过程中常见的问题而提出的一种解决方案,它们是从实际应用中总结出来的一些经验和方法论。设计模式可以帮助开发人员更加容易地解决复杂问题,提高代码的可重用性、可扩展性、可维护性等,从而提高软件开发效率和代码质量。
具体来说,设计模式主要是为了解决以下几类问题:
(1)代码复杂度问题:在软件开发中,代码往往会变得越来越复杂,难以理解和维护。设计模式提供了一些组织代码的方式,让代码结构更加清晰,易于理解和维护。
(2)重用问题:在开发过程中,我们希望能够尽可能地复用代码,减少重复开发的工作量。设计模式提供了一些通用的解决方案,可以让我们更加容易地复用代码。
(3)扩展性问题:软件开发过程中,我们需要不断地对系统进行扩展和改进。设计模式提供了一些可扩展的解决方案,可以让系统更加容易地扩展和改进,同时保持代码的高可读性和可维护性。
(4)协作问题:在多人协作的开发过程中,代码的组织和沟通变得非常重要。设计模式提供了一些标准化的组织方式,可以让开发者更加容易地沟通和协作。设计原则
软件设计原则是指在软件开发过程中遵循的一些通用的、经过验证的规则和指导原则。这些原则旨在提高软件的可维护性、可扩展性、可重用性和可靠性等方面的质量。
以下是一些常见的软件设计原则:
(1)单一职责原则(SRP):一个类应该只有一个职责或只有一个引起变化的原因。
(2)开放-封闭原则(OCP):软件实体(类、模块、函数等)应该是可扩展的,但是不可修改的。
(3)里氏替换原则(LSP):任何基类可以出现的地方,子类一定可以出现,而且不会导致任何错误或异常。
(4)依赖倒置原则(DIP):高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
(5)接口隔离原则(ISP):客户端不应该依赖于它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。
(6)迪米特法则(LoD):一个对象应该对其他对象有最少的了解。通俗地讲,就是一个类对自己依赖的类知道得越少越好。
(7)合成复用原则(CRP):尽量使用对象组合,而不是继承来达到复用的目的。高内聚,低耦合
高内聚低耦合是软件设计中的一个原则,它强调模块内部的联系应该紧密而模块之间的联系应该尽量松散。具体来说,高内聚指的是一个模块内部的各个组成部分之间的联系应该紧密,组成部分之间的关系应该尽量简单。低耦合指的是一个模块与其他模块之间的依赖关系应该尽量松散,即模块之间的耦合度应该尽量低。
高内聚的好处在于,一个模块内部的联系紧密,表示这个模块是一个独立的整体,对外部的干扰最小。同时,当需要对一个模块进行修改时,只需要修改该模块内部的某些部分,不会对其他部分造成影响,从而提高了系统的可维护性和可扩展性。
低耦合的好处在于,模块之间的联系松散,意味着这些模块之间的依赖性较小,当一个模块需要进行修改时,不会对其他模块产生影响,从而提高了系统的可维护性和可扩展性。23种设计模式
- 创造型模式
- 单例模式:确保一个类只有一个实例,并提供该实例的全局访问点
- 工厂方法模式:它定义了一个创建对象的接口,但由子类决定要实例化哪个类。工厂方法把实例化操作推迟到子类
- 抽象工厂模式:创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同
- 建造者模式:生成器(Builder)?模式,封装一个对象的构造过程,并允许按步骤构造
- 原型模式:使用原型实例指定要创建对象的类型,通过复制这个原型来创建新对象
- 结构型模式
- 适配器模式:将一个类的接口, 转换成客户期望的另一个接口。 适配器让原本接口不兼容的类可以合作无间。 对象适配器使用组合, 类适配器使用多重继承
- 代理模式:为另一个对象提供一个替身或占位符以控制对这个对象的访问
- 桥接模式:使用桥接模式通过将实现和抽象放在两个不同的类层次中而使它们可以独立改变
- 装饰器模式:动态地将责任附加到对象上, 若要扩展功能, 装饰者提供了比继承更有弹性的替代方案
- 外观模式:它提供了一个统一的接口,用来访问子系统中的一群接口,从而让子系统更容易使用
- 组合模式:允许你将对象组合成树形结构来表现”整体/部分”层次结构. 组合能让客户以一致的方式处理个别对象以及对象组合
- 享元模式:利用共享的方式来支持大量细粒度的对象,这些对象一部分内部状态是相同的。 它让某个类的一个实例能用来提供许多”虚拟实例”
- 行为型模式
- 观察者模式:在对象之间定义一对多的依赖, 这样一来, 当一个对象改变状态, 依赖它的对象都会收到通知, 并自动更新
- 策略模式:定义了算法族, 分别封闭起来, 让它们之间可以互相替换, 此模式让算法的变化独立于使用算法的客户
- 命令模式:将”请求”封闭成对象, 以便使用不同的请求,队列或者日志来参数化其他对象. 命令模式也支持可撤销的操作
- 中介者模式:使用中介者模式来集中相关对象之间复杂的沟通和控制方式
- 备忘录模式:当你需要让对象返回之前的状态时(例如, 你的用户请求”撤销”), 你使用备忘录模式
- 模版方式模式:在一个方法中定义一个算法的骨架, 而将一些步骤延迟到子类中. 模板方法使得子类可以在不改变算法结构的情况下, 重新定义算法中的某些步骤
- 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素, 而又不暴露其内部的表示
- 状态模式:允许对象在内部状态改变时改变它的行为, 对象看起来好象改了它的类
- 责任链模式:通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象
- 解释器模式:使用解释器模式为语言创建解释器,通常由语言的语法和语法分析来定义
- 访问者模式:当你想要为一个对象的组合增加新的能力, 且封装并不重要时, 就使用访问者模式
- 创造型模式
单例模式
什么是单例设计模式?
单例模式是⼀种创建型设计模式, 它的核⼼思想是保证⼀个类只有⼀个实例,并提供⼀个全局访问点来访问这个实例。
只有⼀个实例的意思是,在整个应⽤程序中,只存在该类的⼀个实例对象,⽽不是创建多个相同类型的对象。
全局访问点的意思是,为了让其他类能够获取到这个唯⼀实例,该类提供了⼀个全局访问点(通常是⼀个静态⽅法),通过这个⽅法就能获得实例。为什么要使⽤单例设计模式呢?
- 全局控制:保证只有⼀个实例,这样就可以严格的控制客户怎样访问它以及何时访问它,简单的说就是对唯⼀实例的受控访问(引⽤⾃《⼤话设计模式》第21章)
- 节省资源:也正是因为只有⼀个实例存在,就避免多次创建了相同的对象,从⽽节省了系统资源,⽽且多个模块还可以通过单例实例共享数据。
- 懒加载:单例模式可以实现懒加载,只有在需要时才进⾏实例化,这⽆疑会提⾼程序的性能。
单例设计模式的基本要求
- 私有的构造函数:防⽌外部代码直接创建类的实例
- 私有的静态实例变量:保存该类的唯⼀实例
- 公有的静态⽅法:通过公有的静态⽅法来获取类的实例
单例模式的实现⽅式有多种,包括懒汉式、饿汉式等。
饿汉式指的是在类加载时就已经完成了实例的创建,不管后⾯创建的实例有没有使⽤,先创建再说,所以叫做“饿汉”。
懒汉式指的是只有在请求实例时才会创建,如果在⾸次请求时还没有创建,就创建⼀个新的实例,如果已经创建,就返回已有的实例,意思就是需要使⽤了再创建,所以称为“懒汉”
饿汉模式(线程安全):类⼀加载就创建对象,这种⽅式⽐较常⽤。
优点:线程安全,没有加锁,执⾏效率较⾼。
缺点:不是懒加载,类加载时就初始化,浪费内存空间。
如何保证线程安全:基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。
1 | public class Singleton { |
懒汉模式(线程安全)
懒汉模式在单线程下使⽤没有问题,对于多线程是⽆法保证单例的。
通过 synchronized 关键字加锁保证线程安全,synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这⾥演示添加在⽅法上⾯,存在的问题是 每⼀次调⽤ getInstance 获取实例时都需要加锁和释放锁,这样是⾮常影响性能的。
优点:懒加载,线程安全。
缺点:效率较低。
1 | public class Singleton { |
什么时候使⽤单例设计模式?
说了这么多,那在什么场景下应该考虑使⽤单例设计模式呢?可以结合单例设计模式的优点来看。
- 资源共享
多个模块共享某个资源的时候,可以使⽤单例模式,⽐如说应⽤程序需要⼀个全局的配置管理器来存储和管理配置信息、亦或是使⽤单例模式管理数据库连接池。 - 只有⼀个实例
当系统中某个类只需要⼀个实例来协调⾏为的时候,可以考虑使⽤单例模式, ⽐如说管理应⽤程序中的缓存,确保只有⼀个缓存实例,避免重复的缓存创建和管理,或者使⽤单例模式来创建和管理线程池。 - 懒加载
如果对象创建本身就⽐较消耗资源,⽽且可能在整个程序中都不⼀定会使⽤,可以使⽤单例模式实现懒加载。
在许多流⾏的⼯具和库中,也都使⽤到了单例设计模式,⽐如Java中的 Runtime 类就是⼀个经典的单例,表示程序的运⾏时环境。此外 Spring 框架中的应⽤上下⽂ ( ApplicationContext ) 也被设计为单例,以提供对应⽤程序中所有 bean 的集中式访问点
工厂方法模式
什么是⼯⼚⽅法模式?
⼯⼚⽅法模式也是⼀种创建型设计模式,简单⼯⼚模式只有⼀个⼯⼚类,负责创建所有产品,如果要添加新的产品,通常需要修改⼯⼚类的代码。
⽽⼯⼚⽅法模式引⼊了抽象⼯⼚和具体⼯⼚的概念,每个具体⼯⼚只负责创建⼀个具体产品,添加新的产品只需要添加新的⼯⼚类⽽⽆需修改原来的代码,这样就使得产品的⽣产更加灵活,⽀持扩展,符合开闭原则。⼯⼚⽅法模式分为以下⼏个⻆⾊:
抽象⼯⼚:⼀个接⼝,包含⼀个抽象的⼯⼚⽅法(⽤于创建产品对象)。
具体⼯⼚:实现抽象⼯⼚接⼝,创建具体的产品。
抽象产品:定义产品的接⼝。
具体产品:实现抽象产品接⼝,是⼯⼚创建的对象。应⽤场景
⼯⼚⽅法模式使得每个⼯⼚类的职责单⼀,每个⼯⼚只负责创建⼀种产品,当创建对象涉及⼀系列复杂的初始化逻辑,⽽这些逻辑在不同的⼦类中可能有所不同时,可以使⽤⼯⼚⽅法模式将这些初始化逻辑封装在⼦类的⼯⼚中。在现有的⼯具、库中,⼯⼚⽅法模式也有⼴泛的应⽤,⽐如:
Spring 框架中的 Bean ⼯⼚:通过配置⽂件或注解,Spring 可以根据配置信息动态地创建和管理对象。
JDBC 中的 Connection ⼯⼚:在 Java 数据库连接中, DriverManager 使⽤⼯⼚⽅法模式来创建数据库连接。不同的数据库驱动(如 MySQL、PostgreSQL 等)都有对应的⼯⼚来创建连接
配适器模式
概念:适配器模式就是将⼀个类的接⼝,转换成客户期望的另⼀个接⼝。
作用:可以让原本两个不兼容的接⼝能够⽆缝完成对接。
原理 or 实现:适配器实现了其中一个对象的接口, 并对另一个对象进行封装。
Java 各版本的新特性
New highlights in Java SE 8
Lambda Expressions,Pipelines and Streams,Date and Time API,Default Methods,Type Annotations,Nashhorn JavaScript Engine,Concurrent Accumulators,Parallel operations,PermGen Error Removed
New highlights in Java SE 7
Strings in Switch Statement,Type Inference for Generic Instance Creation,Multiple Exception Handling,Support for Dynamic Languages,Try with Resources,Java nio Package,Binary Literals,Underscore in literals,Diamond Syntax
Java 与 C++ 的区别
- Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
- Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
- Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
- Java 支持自动垃圾回收,而 C++ 需要手动回收。
- Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
- Java 不支持操作符重载,而 C++ 可以。虽然可以对两个 String 对象支持加法运算,但是这是语言内置支持的操作,非操作符重载
- Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
- Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。
知识体系
Java全栈知识体系:https://pdai.tech/