数据类型
整型
byte:-128 ~ 127
short: -32768 ~ 32767
int: -2147483648 ~ 2147483647
long: -9223372036854775808 ~ 9223372036854775807
1 | public class Main { |
浮点型
1 | float f1 = 3.14f; |
对于 float 类型,需要加上 f 后缀
强制转型
可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。
如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型。
引用类型
除了整型,浮点型,布尔类型,字符类型,其他都为引用类型
判断相等
判断引用类型的变量内容是否相等,必须使用 equals() 方法
字符串
对于多行的字符串,使用 “””…”””表示多行字符串(java13 开始)
1 | String s = """ |
由于多行字符串是作为预览特性(Preview Language Features)实现的,编译的时候,我们还需要给编译器加上参数:
1 | javac --source 14 --enable-preview Main.java |
常量
定义变量的时候,如果加上 final 修饰符,这个变量就变成了常量:
var 关键字
1 | var sb = new StringBuilder(); |
编译器会自动推导类型
数组
自定义排序
1 | public class Basic { |
OOP
传参
引用类型传的是引用,会修改传递的应用类型数据本身
向下转型
1 | Person p1 = new Student(); // upcasting, ok |
为了避免向下转型出错,Java 提供了 instanceof 操作符,可以先判断一个实例究竟是不是某种类型:
覆写
加上@Override 可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。@Override 不是必需的。
所有的 class 最终都继承自 Object,而 Object 定义了几个重要的方法:
- toString():把 instance 输出为 String;
- equals():判断两个 instance 是否逻辑相等;
- hashCode():计算一个 instance 的哈希值。
如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为 final。用 final 修饰的方法不能被 Override:
多态 Polymorphic
Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
记录类
1 | public final class Point { |
使用 final 定义 class,除此以外,还需要正确覆写 equals() 和 hasCode()
反射
class 类
class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时,将其加载进内存。
每加载一种 class,JVM 就为其创建一个 Class 类型的实例,并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:
1 | public final class Class { |
以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class 文件到内存,然后,为 String 类创建一个 Class 实例并关联起来:
1 | Class cls = new Class(String); |
这个 Class 实例是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。
所以,JVM 持有的每个 Class 实例都指向一个数据类型(class 或 interface):
一个 Class 实例包含了该 class 的所有完整信息。由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。
这种通过 Class 实例获取 class 信息的方法称为反射(Reflection)。
获取一个 class 的 Class 实例有三个方法
方法一:直接通过一个 class 的静态变量 class 获取:
1 | Class cls = String.class; |
方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass()方法获取:
1 | String s = "Hello"; |
方法三:如果知道一个 class 的完整类名,可以通过静态方法 Class.forName()获取:
1 | Class cls = Class.forName("java.lang.String"); |
因为 Class 实例在 JVM 中是唯一的,所以,上述方法获取的 Class 实例是同一个实例。可以用==比较两个 Class 实例:
1 | Integer n = new Integer(123); |
用 instanceof 不但匹配指定类型,还匹配指定类型的子类。而用==判断 class 实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用 instanceof 判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用==判断 class 实例。
访问字段
1 | public class Test { |
可以调用 f.setAccessible(true); 改变字段的可访问性
设置字段,可以使用 Field.set(Object, Object) 实现的,其中第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。
调用方法
1 | public class Main { |
如果方法是静态的,无需指定实例对象。invoke 方法传入的第一个参数永远为 null。如果调用非 public 方法,可以先通过 Method.setAccessible(true)设置可访问性
使用反射调用方法时,仍然遵循多台原则,即总是调用实际类型的覆写方法。
调用构造方法
调用 Class.newInstance()的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数,或者不是 public,就无法直接通过 Class.newInstance()来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor 对象和 Method 非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
1 | public class Main { |
多线程
创建
有三种方式
方法一:从 Thread 派生一个自定义类,然后覆写 run()方法:
1 | public class Main { |
方法二:创建 Thread 实例时,传入一个 Runnable 实例:
1 | public class Main { |
或者用 Java8 引入的 lambda 语法进一步简写为:
1 | public class Main { |
可以对线程设定优先级,设定优先级的方法是:
1 | Thread.setPriority(int n) // 1~10, 默认值5 |
线程的状态
Java 线程对象 Thread 的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting 和 Terminated;
通过对另一个线程对象调用 join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用 join()方法会立刻返回。
1 | public class Main { |
中断线程
1 | public class Main { |
守护线程
守护线程是指为其他线程服务的线程。在 JVM 中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
1 | Thread t = new MyThread(); |
线程同步
synchronized 关键字对一个对象进行加锁,保证代码块在任意时刻最多只能有一个线程来执行。
1 | public class Main { |
原子操作
JVM 规范定义了几种原子操作:
- 基本类型(long 和 double 除外)赋值,例如:int n = m;
- 引用类型赋值,例如:List
list = anotherList。
同步方法
使用 synchronized 封装逻辑,这样的类就是线程安全的
1 | public class Counter { |
除了少数情况,大部分类,如 ArrayList 都是非线程安全的
1 | public void add(int n) { |
以上两种写法是等价的
对于 static 方法,是没有 this 实例的,因此 static 方法是针对类而不是实例。但是我们注意到任何一个类都有一个由 JVM 自动创建的 Class 实例,因此,对 static 方法添加 synchronized,锁住的是该类的 Class 实例。上述 synchronized static 方法实际上相当于:
1 | public class Counter { |
死锁
可重入锁
java 的线程锁是可重入的锁。JVM 允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于 Java 的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出 synchronized 块,记录-1,减到 0 的时候,才会真正释放锁。
多线程协调
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
集合
hashmap
hashmap 通过以空间换时间的方式,提高查询速度。它用一个大数组存储所有的 value,并根据 key 直接计算出 value 应该存储在那个索引
在 map 的内部,对 key 做比较是通过 equals() 实现的,正确使用 Map 必须保证:
作为 key 的对象必须正确覆写 equals()方法,相等的两个 key 实例调用 equals()必须返回 true;
作为 key 的对象还必须正确覆写 hashCode()方法,且 hashCode()方法要严格遵循以下规范: - 如果两个对象相等,则两个对象的 hashCode()必须相等; - 如果两个对象不相等,则两个对象的 hashCode()尽量不要相等。 -
上述第一条规范是正确性,必须保证实现,否则 HashMap 不能正常工作。
而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的 hashCode(),会造成 Map 内部存储冲突,使存取的效率下降。
原理
实际上 HashMap 初始化时默认的数组大小只有 16,任何 key,无论它的 hashCode()有多大,都可以简单地通过:
1 | int index = key.hashCode() & 0xf; // 0xf = 15 |
添加超过一定数量的 key-value 时,HashMap 会在内部自动扩容,每次扩容一倍,即长度为 16 的数组扩展为长度 32,由于扩容会导致重新分布已有的 key-value,所以,频繁扩容对 HashMap 的性能影响很大。如果我们确定要使用一个容量为 10000 个 key-value 的 HashMap,更好的方式是创建 HashMap 时就指定容量:
1 | Map<String, Integer> map = new HashMap<>(10000); |
servlet
servletContext
单例对象,在 web 部署启动的时候创建,在 web 工程停止的时候销毁
- 获取 web.xml 配置的 context.param
- 获取当前的工程路径 /
- 获取工程部署后在服务器的绝对路径
- 像 Map 一样存取数据
转发
可以 forward 共享 req 和 resp 对象
可以访问 WEB-INF 目录