0%

java基础

数据类型

整型

byte:-128 ~ 127
short: -32768 ~ 32767
int: -2147483648 ~ 2147483647
long: -9223372036854775808 ~ 9223372036854775807

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
int i = 2147483647;
int i2 = -2147483648;
int i3 = 2_000_000_000; // 加下划线更容易识别
int i4 = 0xff0000; // 十六进制表示的16711680
int i5 = 0b1000000000; // 二进制表示的512
long l = 9000000000000000000L; // long型的结尾需要加L
}
}

浮点型

1
2
3
4
5
float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324

对于 float 类型,需要加上 f 后缀

 强制转型

可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。
如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型。

引用类型

除了整型,浮点型,布尔类型,字符类型,其他都为引用类型

判断相等

判断引用类型的变量内容是否相等,必须使用 equals() 方法

字符串

对于多行的字符串,使用 “””…”””表示多行字符串(java13 开始)

1
2
3
4
5
6
String s = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC
""";

由于多行字符串是作为预览特性(Preview Language Features)实现的,编译的时候,我们还需要给编译器加上参数:

1
javac --source 14 --enable-preview Main.java

常量

定义变量的时候,如果加上 final 修饰符,这个变量就变成了常量:

var 关键字

1
var sb = new StringBuilder();

编译器会自动推导类型

数组

自定义排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Basic {
public static void main(String[] args) {
// 不能使用基本类型,而要使用它们对应的类
Integer[] ns = {1, 2, 3, 4, 5};
for (int n : ns) {
System.out.println(n);
}
Arrays.sort(ns, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
});
System.out.println(Arrays.toString(ns));
}
}

OOP

传参

引用类型传的是引用,会修改传递的应用类型数据本身

向下转型

1
2
3
4
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

为了避免向下转型出错,Java 提供了 instanceof 操作符,可以先判断一个实例究竟是不是某种类型:

覆写

加上@Override 可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。@Override 不是必需的。

所有的 class 最终都继承自 Object,而 Object 定义了几个重要的方法:

  • toString():把 instance 输出为 String;
  • equals():判断两个 instance 是否逻辑相等;
  • hashCode():计算一个 instance 的哈希值。

如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为 final。用 final 修饰的方法不能被 Override:

多态 Polymorphic

Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

记录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Point {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int x() {
return this.x;
}

public int y() {
return this.y;
}
}

使用 final 定义 class,除此以外,还需要正确覆写 equals() 和 hasCode()

反射

class 类

class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时,将其加载进内存。

每加载一种 class,JVM 就为其创建一个 Class 类型的实例,并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:

1
2
3
public final class Class {
private 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
2
String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个 class 的完整类名,可以通过静态方法 Class.forName()获取:

1
Class cls = Class.forName("java.lang.String");

因为 Class 实例在 JVM 中是唯一的,所以,上述方法获取的 Class 实例是同一个实例。可以用==比较两个 Class 实例:

1
2
3
4
5
6
7
Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类

boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class

用 instanceof 不但匹配指定类型,还匹配指定类型的子类。而用==判断 class 实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用 instanceof 判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用==判断 class 实例。

访问字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {

public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value);
}
}

class Person {
public String name;

public Person(String name) {
this.name = name;
}
}

可以调用 f.setAccessible(true); 改变字段的可访问性

设置字段,可以使用 Field.set(Object, Object) 实现的,其中第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。

调用方法

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}

如果方法是静态的,无需指定实例对象。invoke 方法传入的第一个参数永远为 null。如果调用非 public 方法,可以先通过 Method.setAccessible(true)设置可访问性
使用反射调用方法时,仍然遵循多台原则,即总是调用实际类型的覆写方法。

调用构造方法

调用 Class.newInstance()的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数,或者不是 public,就无法直接通过 Class.newInstance()来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor 对象和 Method 非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);

// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}

多线程

创建

有三种方式

方法一:从 Thread 派生一个自定义类,然后覆写 run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}

方法二:创建 Thread 实例时,传入一个 Runnable 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}

或者用 Java8 引入的 lambda 语法进一步简写为:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}

可以对线程设定优先级,设定优先级的方法是:

1
Thread.setPriority(int n) // 1~10, 默认值5

线程的状态

Java 线程对象 Thread 的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting 和 Terminated;
通过对另一个线程对象调用 join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用 join()方法会立刻返回。

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}

中断线程

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
37
38
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}

class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}

守护线程

守护线程是指为其他线程服务的线程。在 JVM 中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

1
2
3
Thread t = new MyThread();
t.setDaemon(true);
t.start();

线程同步

synchronized 关键字对一个对象进行加锁,保证代码块在任意时刻最多只能有一个线程来执行。

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
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}

class Counter {
public static final Object lock = new Object();
public static int count = 0;
}

class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count += 1;
}
}
}
}

class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count -= 1;
}
}
}
}

原子操作

JVM 规范定义了几种原子操作:

  • 基本类型(long 和 double 除外)赋值,例如:int n = m;
  • 引用类型赋值,例如:List list = anotherList。

同步方法

使用 synchronized 封装逻辑,这样的类就是线程安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Counter {
private int count = 0;

public void add(int n) {
synchronized(this) {
count += n;
}
}

public void dec(int n) {
synchronized(this) {
count -= n;
}
}

public int get() {
return count;
}
}

除了少数情况,大部分类,如 ArrayList 都是非线程安全的

1
2
3
4
5
6
7
8
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁

以上两种写法是等价的

对于 static 方法,是没有 this 实例的,因此 static 方法是针对类而不是实例。但是我们注意到任何一个类都有一个由 JVM 自动创建的 Class 实例,因此,对 static 方法添加 synchronized,锁住的是该类的 Class 实例。上述 synchronized static 方法实际上相当于:

1
2
3
4
5
6
7
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}

死锁

可重入锁

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 目录