JavaSE:标准版(桌面程序,控制台)
JavaME:嵌入式开发(手机)
JavaEE:E企业级开发(web,服务器)
JDK(Java Development Kit):整个java开发工具
JRE(Java Runtime Environment):运行时环境
JVM(Java Virtual Machine):java虚拟机
Java 程序其实是运行在JVM (Java虚拟机) 上的,使用 Java 编译器编译 Java 程序时,生成的是与平台无关的字节码,这些字节码只面向 JVM。不同平台的 JVM 都是不同的,但它们都提供了相同的接口,这也正是 Java 跨平台的原因。
普通用户只需要安装 JRE 来运行 Java 程序。而程序开发者必须安装 JDK 来编译、调试程序
JDK目录:
bin
文件里面存放了JDK的各种开发工具的可执行文件,主要的是编译器 (javac.exe
)db
文件是一个先进的全事务处理的基于 Java 技术的数据库(jdk 自带数据库 db 的使用)include
文件里面是 Java 和 JVM 交互用的头文件jre
为 Java 运行环境lib
文件存放的是 JDK 工具命令的实际执行程序
JRE目录:
bin
里的就是 JVMlib
中则是 JVM 工作所需要的类库
源代码的文件名必须与文件中公共类 public class 的名字相同。
在IDE中运行Java程序,IDE自动传入的-classpath
参数是当前工程的目录和引入的jar包。不要把任何Java核心库添加到classpath中,JVM根本不依赖classpath加载核心库
基本类型
8中基本类型使用频率高,存在栈中
基本类型默认值仅在 Java 初始化类的时候才会被赋予,局部变量不会
Java 没有 sizeof
-
整型 byte(1) / short(2) / int(4) / long(8)
在 Java 中, 整型的范围与运行 Java 代码的机器无关(平台无关性)。必须保证在所有机器上都能够得到相同的运行结果, 所以各种数据类型的取值范围必须固定,也没有 sizeof。
长整型:后缀L 十六进制:前缀0x 二进制:前缀0b
数字字面量加下划线(如用 1_000_000 表示一百万),这些下划线只是为了提高可读性,Java 编译器会去除这些下划线
Java 没有任何无符号(unsigned) 形式的 int、 long、short 或 byte 类型。
-
浮点型 float(4) / double(8)
float:后缀f double:后缀d(或默认)
//正无穷大 Double.POSITIVE_INFINITY //负无穷大 Double.NEGATIVE_INFINITY //NaN (不是一个数字) Double.NaN //检测非数字 if(Double.isNaN(x))
-
boolean类型(1/8)
整型值、整数表达式和布尔值之间不能进行相互转换
-
char型(2)
两个字节的Unicode编码,\u为unicode转义
byte[] b1 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换 byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
高精度数值 BigInteger / BigDecimal
可以处理包含任意长度数字序列的数值,且计算中没有任何舍入误差
import java.math.BigInteger;
import java.math.BigDecimal;
//将普通数值转化为大数值
BigInteger a = BigInteger.valueOf(100);
Biglnteger c = a.add(b); // c = a + b
Biglnteger d = c.multiply(b.add(Biglnteger.valueOf(2))); // d = c * (b + 2)
Math包
Math.floorMod(x,y)
总会得到一个正余数
Math.addExact
,Math.subtractExact
,Math.multiplyExact
等可以将溢出抛出为异常进行捕获
内置包装类
在 Java 中,万物皆对象,所有的操作都要求用对象的形式进行描述,为了把基本类型转换成对象,Java 给我们提供了完善的内置包装类
【Java 内置的包装类无法被继承,所有的包装类型都是不变类】
基本类型 | 对应的包装类(位于 java.lang 包中) |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
前 6 个类派生于公共的超类 Number
,而 Character
和 Boolean
是 Object
的直接子类
【创建新对象时,优先选用静态工厂方法而不是new操作符】:
// 总是创建新实例
Integer n = new Integer(100);
// 静态工厂方法
Integer n = Integer.valueOf(100);
- 装箱:将基本数据类型转换成包装类(每个包装类的构造方法都可以接收各自数据类型的变量)
- 拆箱:从包装类之中取出被包装的基本类型数据(使用包装类的 xxxValue 方法)
装箱和拆箱会影响执行效率,且拆箱时可能发生 NullPointerException
// JDK 1.5 之后
// 自动装箱,基本数据类型 int -> 包装类 Integer
// 等价
Integer obj = 10;
Integer obj = Integer.valueOf(10);
// 自动拆箱,Integer -> int
// 等价
int temp = obj;
int temp = obj.intValue();
obj ++; // 直接利用包装类的对象进行数学计算
System.out.println(temp * obj);
【Integer.valueOf】:先判断 i 是否在缓存范围(默认 [-128, 127])内,若在,则返回缓存池中对象(每次地址相同);若不在,则创建新对象(每次new,地址不同)。因此不能用 ==
判断值相等,要用equals()
方法
// Integer.valueOf源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
【Object 类可以接收所有数据类型】:
Object obj = 10;
int temp = (Integer) obj;

Integer 方法
方法 | 描述 |
---|---|
Integer.parseInt(“100”) / Integer.parseInt(“100”, 16) | 字符串转整型 / 指定进制 |
Integer.toBinaryString(100) | 整数转为二进制字符串 |
Character 方法
方法 | 描述 |
---|---|
isLetter() | 是否是一个字母 |
isDigit() | 是否是一个数字字符 |
isWhitespace() | 是否是一个空白字符 |
isUpperCase() | 是否是大写字母 |
isLowerCase() | 是否是小写字母 |
toUpperCase() | 指定字母的大写形式 |
toLowerCase | 指定字母的小写形式 |
Object 通用方法
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
public final native void notify()
public final native void notifyAll()
public final void wait(long timeout, int nanos) throws InterruptedException
-
equals
==
:基本类型时直接判断值,引用类型时判断地址值equals()
:该方法用于判断两个对象是否具有相同的值1):类没有覆盖
equals()
方法。则通过equals()
比较该类的两个对象时,等价于通过==
比较这两个对象(比较的是地址)。2):类覆盖了
equals()
方法。一般来说,我们都覆盖equals()
方法来判断两个对象的内容是否相等,比如String
类。基本类型比较可用
==
,对象比较用n1.equals(n2)
-
hashcode
hashCode() 返回哈希值。一般是将地址变化为整数。
在重写 equals() 方法时应当总是重写 hashCode() 方法,保证等价的两个对象哈希值也相等。
HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象。但是 EqualExample 没有实现 hashCode() 方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。
EqualExample e1 = new EqualExample(1, 1, 1); EqualExample e2 = new EqualExample(1, 1, 1); System.out.println(e1.equals(e2)); // true HashSet<EqualExample> set = new HashSet<>(); set.add(e1); set.add(e2); System.out.println(set.size()); // 2
理想的哈希函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了哈希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。
R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位,最左边的位丢失。并且一个数与 31 相乘可以转换成移位和减法:
31*x == (x<<5)-x
,编译器会自动进行这个优化。@Override public int hashCode() { int result = 17; result = 31 * result + x; result = 31 * result + y; result = 31 * result + z; return result; }
-
toString
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
重载使用valueOf:
class ToStringExample { private int number; @Override public String toString() { return String.valueOf(number); } }
-
clone
默认的拷贝是浅拷贝,没有clone对象中引用的其他对象(可变的子对象会有影响)
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换
可以使用Cloneable接口,拷贝构造函数或者拷贝工厂来拷贝一个对象
public class CloneConstructorExample { public CloneConstructorExample(CloneConstructorExample original) { arr = new int[original.arr.length]; for (int i = 0; i < original.arr.length; i++) { arr[i] = original.arr[i]; } } }
字符串
String
Java的 String 和 char 在内存中总是以Unicode编码表示
在 Java 8 中,String 内部是使用 char 数组来存储数据的;而在 Java 9 之后,String 类的实现改用 byte 数组。不过,无论是 Java 8 还是 Java 9,用来存储数据的 char 或者 byte 数组 value 都一直是被声明为 final 的,这意味着 String中value 数组初始化之后就不能再改变了,并且 String内部没有改变 value 数组的方法。所有 String 的更变都是新建对象:
String a = "hello";
String b = "world";
// 等价于 a = a + b
StringBuilder builder = new StringBuilder();
builder.append(a);
builder.append(b);
a = builder.toString();
// null拼接后会转化"null"
// 把已知编码的byte[]转换为String,不能用b.toString(),这样得到的只是类信息
byte[] b = ...
String s1 = new String(b, "GBK");
空串与null
// 空串是一个 Java 对象, 有自己的串长度(0)和内容(空)
if(str.length() == 0)
// null,目前没有任何对象与该变量关联
if(str == null)
任何一个 Java 对象都可以转换成字符串
// 调用toString,并可以处理null情况
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:为字符串开辟了一个**【字符串常量池 String Pool】**,可以理解为缓存区。创建字符串常量时,首先检查字符串常量池中是否存在该字符串,若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;若不存在,则实例化该字符串并放入池中。
JDK 1.7 之前,字符串常量池存在于【常量存储(Constant storage)】中;JDK 1.7 之后,字符串常量池存在于【堆内存(Heap)】中
String str1 = "hello"; // 分配到常量池中
String str2 = new String(“hello”); // 先在String Pool 中开辟地址空间创建一个字符串对象,指向这个 "hello" 字符串字面量,然后在堆中创建一个字符串对象,使引用指向堆中的对象
String str3 = str2.intern(); // 如果 String Pool 中已经存在一个字符串和该字符串的值相等,那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用
【将String型数据变为基本数据类型】:
String str = "10";
int temp = Integer.parseInt(str);// String -> int
不可变的好处
- 可以缓存 hash 值:如 String 用做 HashMap 的 key,不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算
- String Pool 的需要
- 安全性
- 线程安全:StringBuilder 不是线程安全的;StringBuffer 是线程安全的,内部使用 synchronized 进行同步
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 同StringBuffer,是可变类 |
不可变 | 可变 | 可变 |
线程安全 | 线程安全 | 线程不安全 |
多线程操作字符串 | 单线程操作字符串 | |
执行速度慢 | 执行速度快 |
String的方法
方法 | 描述 |
---|---|
equals() / equalsIgnoreCase() | 比较 / 忽略大小写的比较 |
contains(“aa”) | 是否包含子串 |
indexOf(“aa”) / lastIndexOf(“aa”) | 最先出现子串的下标 / 最后出现子串的下标 |
startsWith(“aa”) / endsWith(“aa”) | 是否以特定子串开头 / 是否以特定子串结尾 |
substring(2, 4) | 提取子串 |
trim() / strip() | 去除首尾空白字符(空白字符:空格,\t,\r,\n)(返回新字符串)/ 也去除中文字符 |
isEmpty() / isBlank() | 是否为空 / 是否为空白 |
replace(‘l’, ‘w’) | 替换子串 |
split("\,") / join("***", arr) | 分割字符串 / 连接字符串数组 |
formatted() / String.format() | 传入其他参数,替换占位符,然后生成新的字符串 |
toCharArray() | 转换为char[] |
数组
int[] a = {1, 2, 3};
int[] b = new int[3] {1, 2, 3};
double[][] b = new double[4][4];
Java 实际上没有多维数组,只有一维数组,多维数组被解释为【数组的数组】
// 由于可以单独地存取数组的某一行, 所以可以让两行交换
int[] temp = b[1];
b[1] = b[2];
b[2] = temp;
// 使用 for each 循环遍历数组
for(int[] row : a) { // 遍历每一行
for(int value : row) { // 遍历每一列
System.out.println(value);
}
}
Arrays 类
import java.util.Arrays;
// 将一维数组转成字符串类型(打印一维数组的所有元素)
Arrays.toString(a);
// 将二维数组转成字符串类型(打印二维数组的所有元素)
Arrays.deepToString(a);
// 数组拷贝,第 2 个参数是新数组的长度
Arrays.copyOf(a, 2 * a.length());
// 对数组中的元素进行排序
Arrays.sort(a);
// 比较数组,相等的条件是元素个数和对应位置的元素都相等
Arrays.equals(a, b);
集合
集合的主要功能:
- 存储不确定数量的数据(可以动态改变集合长度)
- 存储具有映射关系的数据
- 存储不同类型的数据
【集合只能存储引用类型(对象)】,如果存储的是 int
型数据(基本类型),它会被自动装箱成 Integer
类型;而数组既可以存储基本数据类型,也可以存储引用类型
Collection 接口
【单列集合】 java.util.Collection
:元素是孤立存在的,向集合中存储元素采用一个个元素的方式存储。

Collection
接口中定义了一些单列集合通用的方法:
public boolean add(E e); // 把给定的对象添加到当前集合中
public void clear(); // 清空集合中所有的元素
public boolean remove(E e); // 把给定的对象在当前集合中删除
public boolean contains(E e); // 判断当前集合中是否包含给定的对象
public boolean isEmpty(); // 判断当前集合是否为空
public int size(); // 返回集合中元素的个数
public Object[] toArray(); // 把集合中的元素,存储到数组中
-
List
add,remove,get,set,contains,size
【元素有序、可重复】
-
ArrayList:基于【动态数组】实现,支持随机访问。
ArrayList<Integer> arr = new ArrayList<Integer>(Arrays.asList(1,2,3));
-
Vector:和 ArrayList 类似,但它是线程安全的。
-
LinkedList:基于【双向链表】实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。addFirst,addLast,removeFirst,removeLast,getFirst,getLast,set,contains,size
-
-
Queue
返回null:offer,poll,peek;抛出异常:add,remove,element
-
LinkedList:可以用它来实现双向队列 Deque。offerFirst,offerLast,pollFirst,pollLast,peekFirst,peekLast
-
PriorityQueue:基于【堆】结构实现,可以用它来实现优先队列。offer,poll,peek
// 默认,升序队列,小顶堆,小的优先 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 降序队列,大顶堆,大的优先 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
-
-
Set
add,remove,contains
【拒绝添加重复元素,不能通过整数索引来访问,元素无序】
-
HashSet:基于【HashMap 哈希表】实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。add,remove,contains
-
LinkedHashSet:底层是通过 【LinkedHashMap】来实现,具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。add,remove,contains
-
TreeSet:基于【红黑树】实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。first,last
-
转collection为数组:collection.stream().mapToInt(Integer::intValue).toArray();
Map 接口
【双列集合】java.util.Map
:Map
不能包含重复的键,值可以重复;并且每个键只能对应一个值

Map
接口中定义了一些双列集合通用的方法:put,remove,get,containsKey,containsValue
public V put(K key, V value); // 把指定的键与指定的值添加到 Map 集合中
public V remove(Object key); // 把指定的键所对应的键值对元素在 Map 集合中删除,返回被删除元素的值
public V get(Object key); // 根据指定的键,在 Map 集合中获取对应的值
boolean containsKey(Object key); // 判断集合中是否包含指定的键
public Set<K> keySet(); // 获取 Map 集合中所有的键,存储到 Set 集合中
public Set<Map.Entry<K,V>> entrySet(); // 获取 Map 中所有的 Entry 对象的集合
// Entry 对象
public K getKey(); // 获取某个 Entry 对象中的键
public V getValue(); // 获取某个 Entry 对象中的值
-
HashMap:基于【哈希表】实现,节点中存放key、value、hashcode、next,1.8之前数组+链表,1.8及之后将过长的链表转化为红黑树。put,remove,get,containsKey,containsValue
-
LinkedHashMap:使用【双向链表】来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,转为红黑树)
-
HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。**它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,**ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
-
TreeMap:基于【红黑树】实现。
迭代器 Iterator
// collection 这一表达式必须是一个数组或者是一个实现了 Iterable 接口的类对象
for(variable : collection) {
// todo
}
public E next(); // 返回迭代的下一个元素。
public boolean hasNext(); // 如果仍有元素可以迭代,则返回 true
【为什么迭代器不封装成一个类,而是做成一个接口】:Collection
接口有很多不同的实现类,这些类的底层数据结构大多是不一样的,因此,它们各自的存储方式和遍历方式也是不同的,所以我们不能用一个类来规定死遍历的方法。
适配器
java.util.Arrays#asList()
可以把数组类型转换为 List 类型
public static <T> List<T> asList(T... a)
// 不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
List list = Arrays.asList(1, 2, 3);
函数
Java 程序设计语言总是采用按值调用。一个方法不能修改一个基本数据类型的参数,但可以修改引用内字段
方法签名=方法名字+参数列表,返回类型和访问权限不是签名的一部分
// 可变参数
public static int getSum(int... arr) {}
安全随机数
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
时间
Date表示时间点,LocalDate表示日历
LocalDate date = LocalDate.now();
int year = date.getYear();
int month = date.getMonthValue();
int day = date.getDayOfMonth();
Lambda
lambda表达式中的自由变量必须是事实最终变量(一旦初始化就不在改变)
// Comparator
Arrays.sort(arr, (first, second) -> {first.length() - second.length()});
// 键提取器函数Comparator.comparing()
Arrays.sort(arr,
Comparator.comparing(Person::getName)
.thenComparing(Person::getAge))
// Predicate
list.removeIf(e -> e== null);
// Supplier
// 需要时才调用
LocalDate hireDay = Objects.requireNonNullElse(day, () -> new LocalDate(1970,1,1));
方法引用
可等价于lambda
var timer = new Timer(1000, System.out::println);
I/O
接口:Serializable,类:File,OutputStream,InputStream,Writer,Reader
import java.util.Scanner;
import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
//输入对象
Scanner in = Scanner(System.in);
//按行
String name = in.nextLine();
//读单词
String name = in.next();
//读整数
int age = in.nextInt();
//输出
System.out.println("input");
System.out.printf("Hello,%s,Next year, you will be %d",name,age);
//文件I/O
Scanner in = new Scanner(Paths.get("myfile.txt"),"UTF-8");
PrintWriter out = new PrintWriter("file.txt","UTF-8");
out.printf("str");
out.close();
String IO
File f = new File("src/a.txt");
String outs = "101010101010101010";
StringBuilder ins = new StringBuilder();
try (Reader reader = new FileReader(f)) {
int n;
while ((n = reader.read()) != -1) {
ins.append((char)n);
}
}
try (Writer writer = new FileWriter("src/b.txt")) {
writer.write(outs);
}
内存管理
数据存储
String s;
创建的只是引用,并不是对象。如果此时对 s 应用 String 方法,会报错。因为此时 s 没有与任何事物相关联。因此,一种安全的做法是:创建一个引用的同时便进行初始化
栈:存放Java 的对象引用(变量名)和基本数据类型。int a = 3;
:编译器首先会在栈中创建一个变量名为 a 的引用,然后查找有没有字面值为 3 的地址,没找到,就在栈中开辟一个地址存放 3 这个字面值,然后将引用 a 指向 3 的地址。Java 系统必须知道存储在栈内的所有项的确切生命周期,以便上下移动指针。这一约束限制了程序的灵活性,所以 Java 对象并不存储在此。
堆:存放所有的 Java 对象(new出来都存在堆中)。编译器不需要知道存储的数据在堆里存活多长时间
常量存储:存放字符串常量和基本类型常量 public static final
,这样做是安全的,因为它们永远不会被改变。
作用域
作用域由花括号 { }
的位置决定
但在 C/C++ 中,将一个较大作用域的变量隐藏的做法,在 Java 里是不允许的:
{
int x = 12;
{
int x = 123; // 非法
}
}
当用 new
创建一个 Java 对象时,它可以存活于作用域之外。对象的引用 s
在作用域终点就消失了。然而,s
指向的 String
对象仍占据内存空间。我们无法在作用域之后访问这个对象,因为对他唯一的引用已经超出了作用域的范围:
{
String s = new String("aas");
}
静态机制
-
静态变量
-
静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
-
实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
-
-
静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须实现,也就是说它不能是抽象方法。而且只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。
静态机制允许我们无需创建对象就可以直接通过类的引用来调用该方法,使用类名直接引用静态变量或方法是首选方案,因为它强调了静态属性
class StaticTest { static int i = 1; static void increment() { this.i++; } } //共享相同变量i StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest(); //可以通过类名直接引用 StaticTest.i++; StaticTest t = new StaticTest(); StaticTest.increment(); System.out.println(t.i); //输出2
-
静态语句块
静态语句块在类初始化时运行一次。
-
静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
静态方法要使用内部非静态类:
public class outer_class{
class inner_class{
}
public static void main(String[] args) {
inner_class c = new outer_class().new inner_class();
}
}
final
-
数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
-
对于基本类型,final 使数值不变;
-
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1; // x = 2; // cannot assign value to final variable 'x' final A y = new A(); y.a = 1;
-
-
方法
声明方法不能被子类重写。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。
-
类
声明类不允许被继承。
异常处理
异常处理框架基于基类java.lang.throwable
,分为【错误 Error】和【异常 Exception】
Error:表示 JVM 无法处理的严重错误
Exception 分为两种:
- 受检异常 :需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;必须捕获的异常:包括
Exception
及其子类,但不包括RuntimeException
及其子类,这种类型的异常称为Checked Exception;在方法定义的时候,使用throws Xxx
表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错。 - 非受检异常 :是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复。

整数被 0 除将会产生一个异常, 而浮点数被 0 除将会得到无穷大或 NaN 结果
所有异常都可以调用printStackTrace()
方法打印异常栈
多 catch 语句:多个catch
语句只有一个能被执行;子类必须写在前面;
// 合并捕获
catch (IOException | NumberFormatException e){}
finally 语句:有无错误都会执行;在catch
中抛出异常,不会影响finally
的执行。JVM会先执行finally
,然后抛出异常
捕获异常
try {
list1.insertAtIndex(1, 7);
} catch (IndexOutOfBoundsException e) {
testsPassed++;
}
面向对象编程
尽量不要返回可变对象的引用,如果需要,首先应进行克隆:return (Date) hireDay.clone();
初始化
class Test extends Cookie{
private int birthday;
private String sex;
private int i;
// 静态初始化块
static{
System.out.println("Root的静态初始化块");
}
// 非静态初始化块
{
System.out.println("Root的普通初始化块");
}
// 默认构造函数
Test(){ }
// 无参构造函数
public Test(){ }
// 有参构造函数
public Test(int birthday, String sex){
// 先初始化父类
super(age);
this.birthday = birthday;
this.sex = sex;
}
}
一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器
初始化块
首先运行初始化块,然后才运行构造函数的主体部分
初始化顺序:
- 静态变量和静态初始化块:使用 static 定义代码块,只有当类装载到系统时执行一次,之后不再执行。在静态初始化块中仅能初始化 static 修饰的数据成员
- 实例变量和非静态初始化块
- 构造函数
访问控制
包(package)
用来汇聚一组类, package
语句必须是文件中除了注释之外的第一行代码。
访问修饰符(access specifier)
用于修饰被封装的类的访问权限,从“最大权限”到“最小权限”依次是:
- 公开的 -
public
- 受保护的 -
protected
:除了提供包访问权限,而且即使父类和子类不在同一个包下,子类也可以访问父类中具有 protected 访问权限的成员 - 包访问权限(没有关键字):成员可以被同一个包中的所有方法访问,但是这个包之外的成员无法访问
- 私有的 -
private
继承多态
技巧
- 将公共操作和字段放在超类中
- 不要过度使用protected字段
- 使用多态,而不要使用类型信息(instanceof)
将某一个抽象的类,改造成能够适用于不同特定需求的类
class Test extends father{
private int birthday;
private String sex;
private int i;
void info() {
System.out.println("Tree is 3 feet tall");
}
// 重载
@Overload // 该注解写与不写 JVM 都能自动识别方法重载。写上有助于 JVM 检查错误
void info(String s) {
System.out.println(s + ": Tree is " + height + " feet tall");
}
// 重写,在覆盖一个方法的时候,子类方法不能低于父类方法的可见性
@Override
void father_method() {
// 在子类的重写方法中调用父类的被重写的方法
super.father_method();
System.out.println("ok");
}
// this,只能在非静态方法内部使用
// 也可在构造函数中调用另一个构造函数
Test increment() {
i++;
return this;
}
}
// 向上转型
// 自动转换,一种多态
// 父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。
Animal animal = new Cat(...);
// 向下转型
// 需要调用一些子类特有而父类没有的方法
Cat cat = (Cat) animal;
【向下转型注意!】
对于java的向下转型,强制向下转型,编译不会报错。但是在运行时,如果对象本身并非是该类型,强制转型,在运行时会报java.lang.ClassCastException。因此只有把子类型赋给父类的引用,然后把该引用向下转型的情况,才不会报错。父类不能强制向下转型!向上转型则是安全的。
【Java 不支持多重继承】
如果一个子类拥有多个父类的话,那么当多个父类中有重复的属性或者方法时,子类的调用结果就会含糊不清,也就是存在【二义性】;将会使语言变得复杂,或者效率降低
【Java 不支持操作符重载】
从Java 15开始,允许使用sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称。
组合
在新类中创建现有类的对象,表示出来的是一种明确的**【has-a】**的关系
public class Cat {
// 组合
private Animal animal;
// 使用构造函数初始化成员变量
public Cat(Animal animal){
this.animal = animal;
}
// 通过调用成员变量的固有方法使新类具有相同的功能
public void breath(){
animal.breath();
}
// 为新类增加新的方法
public void run(){
System.out.println("I'm running");
}
}
慎用继承,优先使用组合
使用继承就无法避免以下这两个问题:
1)打破了封装性,违反了 OOP 原则。迫使开发者去了解父类的实现细节,子类和父类耦合
2)父类更新后可能会导致一些不可知的错误
父类中可覆盖的方法调用了别的可覆盖的方法,这时候如果子类覆盖了其中的一些方法,就可能导致错误
抽象
抽象类
// 抽象类
public abstract class Person {
// 抽象方法
public abstract String getDescription();
}
// 非抽象类
public class Student extends Person {
private String major;
public Student(String name, String major) {
super(name);
this.major = major;
}
@Override
public String getDescription(){ // 实现父类抽象方法
return "a student majoring in " + major;
}
}
Person p = new Student("Jack","Computer Science");// p 引用的是 Student 这样的【具体子类对象】
// 检查一个对象是否属于某个特定类或接口
if(x instanceof Student){
...
}
接口
// 接口
interface Concept {
void idea1();
void idea2();
}
class Implementation implements Concept {
@Override
public void idea1() {
System.out.println("idea1");
}
@Override
public void idea2() {
System.out.println("idea2");
}
}
// 向上转型并调用
Concept c = new Implementation();
c.idea1();
接口将被自动被设置为 abstract
类型;
接口中的【字段】将被自动被设置为 public static final
类型;
接口中的【方法】将被自动被设置为 public abstract
类型,并且不允许定义为 private 或者 protected。从 Java 9 开始,允许将方法定义为 private,这样就能定义某些复用的代码又不会把方法暴露出去。
一个类只能继承一个父类,但是一个类可以实现**【多个接口】**,这也是为什么要区别接口和抽象类
在 Java 8 中,允许在接口中增加静态方法(static,实用方法的实现)和默认方法(default,接口演化,)。当冲突时:
1 ) 【超类优先】:如果超类提供了一个具体方法,接口中的同名且有相同参数类型的默认方法会被忽略。
2 ) 【接口冲突】:如果一个父类接口提供了一个默认方法,另一个父类接口也提供了一个同名而且参数类型相同的方法,子类必须覆盖这个方法来解决冲突。
【函数式接口】:只有一个抽象方法的接口,可以提供lambda表达式来创建接口对象
常用接口:
①Comparable接口
class Employee implements Comparable<Employee>{
@Override
public int compareTo(Employee o) {
return Double.compare(salary, o.salary);
}
}
②Comparator接口(比较器)
class LengthComparator implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
var cmp = new LengthComparator;
if(cmp.compare(s1,s2) > 0){}
③Cloneable接口(标记接口,不包含任何方法)
class Employee implements Cloneable{
@Override
public Employee clone() throws CloneNotSupportedException {
//call Object.clone()
Employee cloned = (Employee) super.clone();
// clone mutable fields
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
内部类
内部类可以对同一个包中的其他类隐藏,可以访问原本私有的数据
-
Inner Class
依附于Outer Class的实例,即隐含地持有
Outer.this
实例,并拥有Outer Class的private
访问权限;可用于 interface 内部(默认 static),也可用于 class 内部 -
Anonymous Class
// 匿名类继承 Runnable 接口 Runnable r = new Runnable() { @Override public void run() { System.out.println("Hello, " + Outer.this.name); } }; // 匿名类继承HashMap,并用 static 代码块来初始化数据 HashMap<String, String> map3 = new HashMap<>() { { put("A", "1"); put("B", "2"); } };
-
Static Nested Class
独立类,但拥有Outer Class的
private
访问权限。
JavaBean
属性 private,通过 public 的 get 和 set 方法来访问
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
:
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
枚举
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
// enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较
if (day == Weekday.FRI) {}
// 返回常量名
String s = Weekday.SUN.name(); // "SUN"
// 返回定义的常量的顺序
int n = Weekday.MON.ordinal(); // 1
record
从Java 14开始,引入了新的Record
类,用来创建不变类
// 定义一个Point类,有x、y两个变量
public record Point(int x, int y) {}
反射机制
优点:
- 可扩展性:应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。比较灵活,能够在运行时动态获取类的实例。
- 类浏览器和可视化开发环境:一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
- 调试器和测试工具: 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。
缺点:
尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。
- 性能开销:反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性和封装性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
Class类
在程序运行期间,JVM 始终为所有的对象维护一个【运行时的类型标识】,这个信息跟踪着每个对象所属的类的完整结构信息,包括包名、类名、实现的接口、拥有的方法和字段等。
可以把 Class 类理解为【类的类型】,一个 Class 对象,称为类的类型对象,一个 Class 对象对应一个加载到 JVM 中的一个 .class 文件。
import java.util.Date; // 先有类
public class Test {
public static void main(String[] args) {
Date date = new Date(); // 后有对象
System.out.println(date);
}
}
首先 JVM 会将你的代码编译成一个 .class
字节码文件,然后被类加载器(Class Loader)加载进 JVM 的内存中,同时会创建一个 Date
类的 Class
对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。JVM 在创建 Date
对象前,会先检查其类是否加载,寻找类对应的 Class
对象,若加载好,则为其分配内存,然后再进行初始化 new Date()
。每个类只有一个 Class
对象,如果有第二条 new Date()
语句,JVM 不会再生成一个 Date
的 Class
对象。
反射的含义:可以通过这个 Class
对象看到类的结构
获取 Class 类对象
// 1.类名.class,知道具体类
Class alunbarClass = TargetObject.class;
// 2.通过 Class.forName() 传入全类名获取
Class alunbarClass1 = Class.forName("com.xxx.TargetObject");
// 3.通过对象实例 instance.getClass() 获取
Class alunbarClass2 = date.getClass();
// 4.通过类加载器 xxxClassLoader.loadClass() 传入类路径获取
Class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");
反射获得类结构
Constructor
Class<Object> clazz = (Class<Object>) Class.forName("fanshe.Student");
// 1.创建一个与 clazz 具有相同类类型的实例
// newInstance 方法【调用默认的构造函数(无参构造函数)】初始化新创建的对象。如果这个类没有默认的构造函数, 就会抛出一个异常
Date date2 = clazz.getDeclaredConstructor().newInstance();
// 2.如果需要调用类的带参构造函数、私有构造函数等,就需要采用 Constractor.newInstance()
// 1)获取所有"公有的"构造方法
Constructor[] conArray = clazz.getConstructors();
// 2)获取所有的构造方法(包括私有、受保护、默认、公有)
Constructor[] conArray = clazz.getDeclaredConstructors();
// 3)获取一个指定参数类型的"公有的"构造方法
Constructor con = clazz.getConstructor(null); // 无参的构造方法类型是一个null
// 4)获取一个指定参数类型的"构造方法",可以是私有的,或受保护、默认、公有
Constructor con = clazz.getDeclaredConstructor(int.class);
con.setAccessible(true); // 为了调用 private 方法/域 我们需要取消安全检查
// 使用开源库 Objenesis
Objenesis objenesis = new ObjenesisStd(true);
Test test = objenesis.newInstance(Test.class);
test.show();
Field
Class<Object> clazz = (Class<Object>) Class.forName("fanshe.Student");
// 获取所有公有的字段
Field[] fieldArray = clazz.getFields();
// 获取所有的字段(包括私有、受保护、默认的)
Field[] fieldArray = clazz.getDeclaredFields();
// 获取一个指定名称的公有的字段
Field f = clazz.getField("name");
// 获取一个指定名称的字段,可以是私有、受保护、默认的
Field f = clazz.getDeclaredField("phoneNum");
f.setAccessible(true); // 暴力反射,解除私有限定
f.set(obj, "刘德华"); // 为 Student 对象中的 name 属性赋值
Method
Class<Object> clazz = (Class<Object>) Class.forName("fanshe.Student");
// 获取所有"公有方法"(包含父类的方法,当然也包含 Object 类)
Method[] methodArray = clazz.getMethods();
// 获取所有的成员方法,包括私有的(不包括继承的)
Method[] methodArray = clazz.getDeclaredMethods();
// 获取一个指定方法名和参数类型的成员方法:
Method m = clazz.getMethod("name");
// 调用任意方法和构造器
m.invoke(obj, "小牛肉");
Modifier
// 返回整数,描述这个类/字段/方法/构造器的修饰符,使用Modifier类分析返回值
Class/Field/Method/Constructor.getModifiers()
泛型
泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同
Java语言的泛型实现方式是擦拭法(Type Erasure)
所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T
视为Object
处理,但是,在需要转型的时候,编译器会根据T
的类型自动为我们实行安全地强制转型
// 向上转型,自动推断
List<Number> list = new ArrayList<>();
// 错误,ArrayList<Integer>不继承ArrayList<Number>
ArrayList<Number> list2 = new ArrayList<Integer>();
// 泛型类
public class Box<T> {
private T t;
public Box(T t){ this.t = t;}
public void set(T t) { this.t = t; }
public T get() { return t; }
// 静态泛型方法,可定义在一般类中
// 泛型 返回类型 泛型参数
public static <K> Box<K> create(K t) {
return new Box<K>(t);
}
// extends通配符,使得方法接收所有泛型类型为Number或Number子类的Pair类型,用来将参数设定为只读
static int add(Box<? extends Number> p){
// 编译不通过,不能将Integer类型转为Number的任意子类
p.set(new Integer(100));
}
}
泛型接口
除了ArrayList<T>
使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])
可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>
这个泛型接口:
public interface Comparable<T> {
/**
* 返回负数: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回正数: 当前实例比参数o大
*/
int compareTo(T o);
}
Java泛型的局限
局限一:<T>
不能是基本类型,例如int
,因为实际类型是Object
,Object
类型无法持有基本类型;
局限二:无法取得带泛型的Class
,无论T
的类型是什么,getClass()
返回同一个Class
实例;
局限三:无法判断带泛型的类型;
局限四:不能实例化T
类型,要实例化T
类型,我们必须借助额外的Class<T>
参数:
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
上述代码借助Class<T>
参数并通过反射来实例化T
类型,使用的时候,也必须传入Class<T>
。例如:
Pair<String> pair = new Pair<>(String.class);
extends通配符
上界,用来将方法的参数设定为只读
表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。
// 限定 T 类型为Number或Number子类
public class Box<T extends Number> {
// 使得方法接收所有泛型类型为Number或Number子类
static int add(Box<? extends Number> p){
// 编译不通过,不能将Integer类型转为Number的任意子类
p.set(new Integer(100));
}
}
super通配符
下界,用来将方法的参数设定为只写
表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。
// 接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
PECS原则:Producer Extends Consumer Super
即:如果需要返回T
,它是生产者(Producer),要使用extends
通配符;如果需要写入T
,它是消费者(Consumer),要使用super
通配符。
注解
https://www.liaoxuefeng.com/wiki/1252599548343744/1255945389098144
Java语言使用@interface
语法来定义注解 Annotation
:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
元注解
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
最常用的元注解是@Target
。使用@Target
可以定义Annotation
能够被应用于源码的哪些位置:
- 类或接口:
ElementType.TYPE
; - 字段:
ElementType.FIELD
; - 方法:
ElementType.METHOD
; - 构造方法:
ElementType.CONSTRUCTOR
; - 方法参数:
ElementType.PARAMETER
。
序列化
需要实现Serilizable接口
// 序列化对象到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./test"));
oos.writeObject(user);
// 序列化文件到对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./test"));
User newUser = (User)ois.readObject();
transient
将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
transient的作用就是把这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
**静态变量是不会被序列化的,即使没有transient关键字修饰。**因为序列化和反序列化的都是对象,并不是类,所以static修饰的变量不会被序列化和反序列化
Lombok
自动生成方法
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
// 自动为所有字段添加@ToString, @EqualsAndHashCode, @Getter方法,为非final字段添加@Setter,和@RequiredArgsConstructor
@Data
// 自动生成全参数构造函数
@AllArgsConstructor
// 自动生成无参数构造函数
@NoArgsConstructor
public class Student {
// 自动生成Getter Setter方法
@Getter @Setter private String name;
// 自动调用close()方法
@Cleanup InputStream in = new FileInputStream(args[0]);
// 避免空指针
public NonNullExample(@NonNull Person person){}
}
Java11新特性
-
本地变量类型推断
var
-
字符串加强
增加了一系列字符串方法
-
集合加强
为集合(List/ Set/ Map)都添加了 of 和 copyOf 方法,它们两个都用来创建不可变的集合
-
Stream 加强
-
Optional 加强
-
InputStream 加强
-
HTTP Client API