参考链接:https://liaoxuefeng.com/books/java/introduction/index.html
都是直接粘贴的廖老师的教程 留作后续复习时看而已
语法基础
变量类型及运算基础
final, static
final 常量 不可修改 一般变量名用大写
修饰符 可变性 生命周期 作用范围 final 不可变(赋值一次) 对象级别 对象的实例方法 static 可变 类级别(共享变量) 类的所有方法 final static 不可变(赋值一次) 类级别(共享常量) 类的所有方法
逻辑运算
还有一种无符号的右移运算,使用»>,它的特点是不管符号位,右移后高位总是补0,因此,对一个负数进行»>右移,它会变成正数,原因是最高位的1变成了0
非运算的规则是,0和1互换: n = ~0; // 1 n = ~1; // 0 异或运算的规则是,如果两个数不同,结果为1,否则为0: n = 0 ^ 0; // 0 n = 0 ^ 1; // 1 n = 1 ^ 0; // 1 n = 1 ^ 1; // 0
在Java的计算表达式中,运算优先级从高到低依次是:
()! ~ ++ --* / %+ -<< >> >>>&|+= -= *= /=记不住也没关系,只需要加括号就可以保证运算的优先级正确。
浮点数0.1在计算机中就无法精确表示,因为十进制的0.1换算成二进制是一个无限循环小数,很显然,无论使用float还是double,都只能存储一个0.1的近似值。但是,0.5这个浮点数又可以精确地表示。
需要特别注意,在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况。例如: double d = 1.2 + 24 / 5; // 结果不是 6.0 而是 5.2 计算结果为5.2,原因是编译器计算24 / 5这个子表达式时,按两个整数进行运算,结果仍为整数4。 要修复这个计算结果,可以将24 / 5改为24.0 / 5。由于24.0是浮点数,因此,计算除法时自动将5提升为浮点数。
Java的字符类型char是基本类型,字符串类型String是引用类型; 基本类型的变量是“持有”某个数值,引用类型的变量是“指向”某个对象;
|
|
表面上看,上面的while循环是一个死循环,但是,Java的int类型有最大值,达到最大值后,再加1会变成负数,结果,意外退出了while循环
换行符
在 Unix/Linux 系统中,换行符(Line Feed,LF) 实际上会将光标移到下一行的行首。尽管从字面上看,“Line Feed” 只表示“换到下一行”,但在实际效果上,它不仅会移动到下一行,还会移动到行首。
因此,在 Unix/Linux 系统中,单个 LF 符号(即 \n)的作用相当于 Windows 中的 CR + LF 组合(即 \r\n)。它既完成了换到下一行的动作,也自动将光标移到该行的行首。
字符串不可变
关于字符串不可变是如何理解的:
|
|
解释: 在这个代码中,t 的值是 “hello”,不会是 “world”。这是因为 字符串在 Java 中是不可变的(即 String 是不可变对象)。 解释:
- String s = “hello”;
- 这行代码创建了一个字符串对象 “hello”,并将 s 指向这个字符串。
- String t = s;
- 这行代码并没有创建一个新的字符串对象,而是将 t 也指向与 s 相同的字符串对象 “hello”。
- s = “world”;
- 这行代码创建了一个新的字符串对象 “world”,并将 s 指向这个新对象。但 t 仍然指向之前的 “hello” 对象,因为 t 并未改变。
- System.out.println(t);
- 因为 t 仍然指向最初的 “hello”,所以输出 “hello”。
字符串不可变: 在Java中,String对象是不可变的,意思是一旦创建,字符串的内容不能被修改。这是通过以下两点实现的:
- 内部使用
private final char[]:String类在内部使用一个char数组来存储字符串内容,这个数组是private和final,意味着它是私有的,外部不能直接访问它,且它不能被修改或重新赋值。 - 没有任何修改字符串内容的方法:String类中没有提供可以直接修改字符数组的方法,像replace()、toUpperCase()这样的方法并不会修改原来的String对象,而是创建一个新的String对象并返回。 例子:
|
|
s = s.toUpperCase();:
- toUpperCase()方法不会修改原来的String对象,而是创建并返回一个新的String对象,内容是"HELLO"。
- 这个新创建的字符串被赋值给s,而原来的"Hello"字符串仍然存在于字符串常量池中,不会被修改。 System.out.println(s);:
- 现在s引用的是新的String对象,内容是"HELLO",因此输出HELLO。
Java整数缓存机制(Integer Cache)
Java为Integer对象提供了一个缓存池,默认情况下,对于值在-128到127范围内的Integer,Java会重用这些对象,而不是每次都创建新的对象。也就是说,当你创建一个值为1的Integer对象时,如果它在-128到127的范围内,JVM会从缓存池中获取一个已经存在的对象,而不是新建一个。这就导致了在这个范围内的Integer对象用两个=号比较时,会返回true,因为它们实际上是指向同一个内存地址。
其他
|
|
始终牢记:Java的String和char在内存中总是以Unicode编码表示。
用指定分隔符拼接字符串数组时,使用StringJoiner或者String.join()更方便;
引用类型可以赋值为null,表示空,但基本类型不能赋值为null。所有的包装类型都是不变类
Integer n = 100; // 编译器自动使用Integer.valueOf(int) int x = n; // 编译器自动使用Integer.intValue() 这种直接把int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数
在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等。必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。
因为我们创建Random实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。 如果我们在创建Random实例时指定一个种子,就会得到完全确定的随机数序列。需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!
数组
打印数组内容用:Arrays.toString(ns),如果直接打印ns,会是打印在jvm的地址
数组排序:Arrays.sort(ns);
二维数组的每个数组元素的长度并不要求相同,例如,可以这么定义ns数组:
|
|
打印二维数组:Arrays.deepToString()
一个Java源文件可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。
可变参数用类型…定义,可变参数相当于数组类型:
|
|
上面的setNames()就定义了一个可变参数。调用时,可以这么写: Group g = new Group(); g.setNames(“Xiao Ming”, “Xiao Hong”, “Xiao Jun”); // 传入3个String g.setNames(“Xiao Ming”, “Xiao Hong”); // 传入2个String g.setNames(“Xiao Ming”); // 传入1个String g.setNames(); // 传入0个String
面向对象
小知识
方法中定义的变量一定要初始化,类中定义的变量可不用初始化,会有默认值
不推荐设置系统环境变量classpath,始终建议通过-cp命令传入; jar包相当于目录,可以包含很多.class文件,方便下载和使用;
|
|
我们在方法内部实例化了一个Runnable。Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它。
- 静态代码块:用于类级别的初始化,在类加载时执行,只执行一次。适用于需要在类加载时进行的一次性操作。
- 实例代码块:用于实例级别的初始化,在每次创建对象时执行,优先于构造方法。适用于需要在对象创建时进行的初始化操作。
构造方法
|
|
由于构造方法是如此特殊,1、所以构造方法的名称就是类名。2、构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,3、构造方法没有返回值(也没有void),调用构造方法,4、必须用new操作符。
如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法。如果既要能使用带参数的构造方法,又想保留不带参数的构造方法,那么只能把两个构造方法都定义出来。调用其他构造方法的语法是this(…)
|
|
继承
具有has关系不应该使用继承,而是使用组合,即Student可以持有一个Book实例: class Student extends Person { protected Book book; protected int score; } 因此,继承是is关系,组合是has关系。
子类如果定义了一个与父类方法签名完全相同的方法(包括返回值),被称为覆写(Override)。
方法名相同,方法参数相同,但方法返回值不同,也是不同的方法。在Java程序中,出现这种情况,编译器会报错。
方法重载是指在同一个类中定义多个方法名相同但参数列表不同的方法。方法重载的关键是参数列表的不同,包括参数的类型、数量和顺序。然而,返回值类型本身不能作为方法重载的唯一依据。也就是说,仅通过返回值类型的不同不能实现方法的重载。
类会自动继承父类的所有字段和方法。然而,如果子类中定义了与父类中已有字段同名的字段,这并不会导致编译错误。相反,子类中的字段会“隐藏”父类中的字段。通常建议避免在子类中定义与父类同名的字段
在Java中,没有明确写extends的类,编译器会自动加上extends Object。所以,任何类,除了Object,都会继承自某个类。Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。注意,允许一个类实现多个接口
子类无法访问父类的private字段或者private方法,用protected修饰的字段可以被子类访问,以及子类的子类所访问
任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();,所以,Student类的构造方法实际上是这样:
|
|
但是,Person类并没有无参数的构造方法,因此,编译失败。 解决方法是调用Person类存在的某个构造方法。 因此,如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。 子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。 从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。
当一个类被修饰为 final 时,表示该类不能被继承。方法也可以被修饰为 final,这意味着该方法不能被子类重写(即不能在子类中提供新的实现)。对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改,可以在构造方法中初始化final字段
执行顺序为:
- 父类静态代码块、静态变量 ps:按声明顺序执行
- 子类静态代码块、静态变量 ps:按声明顺序执行
- 父类局部代码块、成员变量 ps:按声明顺序执行
- 父类构造函数
- 子类局部代码块、成员变量 ps:按声明顺序执行
- 子类构造函数
向上/向下转型
把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。 把一个父类类型强制转型为子类类型,就是向下转型(downcasting)。 Person p1 = new Student(); // upcasting, ok Person p2 = new Person(); Student s1 = (Student) p1; // ok Student s2 = (Student) p2; // runtime error! ClassCastException! 如果测试上面的代码,可以发现: Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。 因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。
多态
public void runTwice(Person p) { p.run(); p.run(); } 它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类例如Teacher,因此,也无法确定调用的是不是Person类定义的run()方法。 所以,多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法。
因为所有的class最终都继承自Object,而Object定义了几个重要的方法:
- toString():把instance输出为String;
- equals():判断两个instance是否逻辑相等;
- hashCode():计算一个instance的哈希值。
关于子类继承父类中方法的调用,只要不是用super调用,调用的都是子类中覆写父类的方法。
抽象、接口、静态
题外话:Collection, Iterator, Map是三种接口,而Set和List继承了Collection接口
把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。必须把Person类本身也声明为abstract,才能正确编译它。抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。一个interface可以继承自另一个interface。 在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象: List list = new ArrayList(); // 用List接口引用具体子类的实例 Collection coll = list; // 向上转型为Collection接口 Iterable it = coll; // 向上转型为Iterable接口
|
|
实现类可以不必覆写default方法。default方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例 不推荐用实例变量.静态字段去访问静态字段,因为在Java程序中,实例对象并没有静态字段。在代码中,实例对象能访问静态字段只是因为编译器可以根据实例类型自动转换为类名.静态字段来访问静态对象。
因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。通过实例变量也可以调用静态方法,但这只是编译器自动帮我们把实例改写成类名而已。
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型:
|
|
实际上,因为interface的字段只能是public static final类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
|
|
包作用域
位于同一个包(package)的类,可以访问包作用域的字段和方法。不用public、protected、private修饰的字段和方法就是包作用域。
编写class的时候,编译器会自动帮我们做两个import动作:
- 默认自动import当前package的其他class;
- 默认自动import java.lang.*。
由于Java支持嵌套类,如果一个类内部还定义了嵌套类,那么,嵌套类拥有访问private的权
如果你不声明public、protected或private,Java中的默认访问修饰符就是包访问权限(default或package-private),意味着类或成员只能被同一个包中的其他类访问。
异常
从继承关系可知:Throwable是异常体系的根,它继承自Object。Throwable有两个体系:Error和Exception,Error表示严重的错误,程序对此一般无能为力,例如:
- OutOfMemoryError:内存耗尽
- NoClassDefFoundError:无法加载某个Class
- StackOverflowError:栈溢出
而Exception则是运行时的错误,它可以被捕获并处理。 某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
- NumberFormatException:数值类型的格式错误
- FileNotFoundException:未找到文件
- SocketException:读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
- NullPointerException:对某个null的对象调用方法或字段
- IndexOutOfBoundsException:数组索引越界
Exception又分为两大类:
- RuntimeException以及它的子类;
- 非RuntimeException(包括IOException、ReflectiveOperationException等等)
Java规定:
- 必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。(也可以捕获,不强制)
JVM在捕获到异常后,会从上到下匹配catch语句,匹配到某个catch后,执行catch代码块,然后不再继续匹配。存在多个catch的时候,catch的顺序非常重要:子类必须写在前面。
通过printStackTrace()可以打印出方法的调用栈
单元测试
Java断言的特点是:断言失败时会抛出AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。断言很少被使用,更好的方法是编写单元测试。
类似@Disabled这种注解就称为条件测试(Conditional Test),JUnit根据不同的条件注解,决定是否运行当前的@Test方法。
日志相关
java.util.logging
因为Java标准库内置了日志包java.util.logging,我们可以直接用。先看一个简单的例子:
|
|
日志级别:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。 Commons Logging的特色是,它可以挂接不同的日志系统,并通过配置文件指定挂接的日志系统。默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。 使用Commons Logging只需要和两个类打交道,并且只有两步: 第一步,通过LogFactory获取Log类的实例; 第二步,使用Log实例的方法打日志。 示例代码如下:
|
|
日志级别:
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
Log4j
前面介绍了Commons Logging,可以作为“日志接口”来使用。而真正的“日志实现”可以使用Log4j。 Log4j是一种非常流行的日志框架,最新版本是2.x。 要打印日志,只需要按Commons Logging的写法写,不需要改动任何代码,就可以得到Log4j的日志输出
SLF4J和Logback
SLF4J和Logback可以取代Commons Logging和Log4j; 我们先来看看SLF4J对Commons Logging的接口有何改进。在Commons Logging中,我们要打印日志,有时候得这么写: int score = 99; p.setScore(score); log.info(“Set score " + score + " for Person " + p.getName() + " ok.”); 拼字符串是一个非常麻烦的事情,所以SLF4J的日志接口改进成这样了: int score = 99; p.setScore(score); logger.info(“Set score {} for Person {} ok.”, score, p.getName()); 我们靠猜也能猜出来,SLF4J的日志接口传入的是一个带占位符的字符串,用后面的变量自动替换占位符,所以看起来更加自然。 如何使用SLF4J?它的接口实际上和Commons Logging几乎一模一样
反射
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
除了int等基本类型外,Java的其他类型全部都是class(包括interface)
获取一个class的Class实例
这种通过Class实例获取class信息的方法称为反射(Reflection)。 如何获取一个class的Class实例?有三个方法: 方法一:直接通过一个class的静态变量class获取: Class cls = String.class; 方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取: String s = “Hello”; Class cls = s.getClass(); 方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取: Class cls = Class.forName(“java.lang.String”);
创建对应类型的实例
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例: // 获取String的Class实例: Class cls = String.class; // 创建一个String实例: String s = (String) cls.newInstance(); 上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
拿到一个实例对应的该字段的值
利用反射拿到字段的一个Field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
|
|
当我们获取到一个Method对象时,就可以对它进行调用。我们以下面的代码为例: String s = “Hello world”; String r = s.substring(6); // “world” 如果用反射来调用substring方法,需要以下代码:
|
|
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例
通过Class对象可以获取继承关系:
Class getSuperclass():获取父类类型;Class[] getInterfaces():获取当前类实现的所有接口。 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
动态代码
还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。
|
|
注解
注解(Annotation) Java的注解可以分为三类:
- 第一类是由编译器使用的注解,例如:
- @Override:让编译器检查该方法是否正确地实现了覆写;
- @SuppressWarnings:告诉编译器忽略此处代码产生的警告。 这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。
-
第二类是由工具处理.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
-
第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
Java语言使用@interface语法来定义注解(Annotation),它的格式如下:
|
|
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
集合
遍历
|
|
我们知道,Map用于存储key-value的映射,对于充当key的对象,是不能重复的,并且,不但需要正确覆写equals()方法,还要正确覆写hashCode()方法。
- 如果不覆写 equals() 和 hashCode(),HashMap 会基于对象的内存地址来进行比较,这会导致即使内容相同的对象在 HashMap 中被认为不同,查找也会失败。
- 通过覆写 equals() 和 hashCode(),可以确保对象按其内容比较,使得 HashMap 能够正确存储和查找相同内容的对象。
如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set。因为放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set。
队列和栈
PriorityQueue和Queue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。
允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名Deque
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
流
流接口
InputStream / OutputStream IO流以byte(字节)为最小单位,因此也称为字节流 如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流。 Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。
注意Windows平台使用\作为路径分隔符,在Java字符串中需要用\\表示一个\。Linux平台使用/作为路径分隔符
因为Windows和Linux的路径分隔符不同,File对象有一个静态变量用于表示当前平台的系统分隔符:
System.out.println(File.separator); // 根据当前平台打印""或"/"
要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下: public abstract int read() throws IOException; 这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
序列化
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:
|
|
序列化 把一个Java对象变为byte[]数组,需要使用ObjectOutputStream 反序列化 ObjectInputStream负责从一个字节流读取Java对象
转换
既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:
|
|
日期格式
特性 SimpleDateFormat DateTimeFormatter 引入版本 Java 1.0 Java 8 线程安全 不是线程安全的 线程安全的 API 设计 过时的设计,和新日期时间 API 不兼容 现代设计,与 java.time API 兼容 日期类型 java.util.Date LocalDate, LocalDateTime, ZonedDateTime 等 灵活性 只能处理 Date 类 处理新的日期和时间类,支持复杂的格式 推荐使用 不推荐,除非需要兼容旧代码 推荐,尤其是在现代应用程序中
- LocalDateTime(不带时区)
- 描述:只包含日期和时间,不含时区信息。
- 示例:2024-09-16T15:30
- 适用场景:日常本地日期和时间的表示,如日程表、公司内部系统的操作时间等。
- ZonedDateTime(带时区)
- 描述:包含日期、时间和时区,可以唯一标识全球范围的某一时刻。
- 示例:2024-09-16T15:30:00+08:00[Asia/Shanghai]
- 适用场景:跨时区应用场景,如航班时间、全球会议、国际交易时间。
- OffsetDateTime(带偏移量)
- 描述:包含日期、时间和时区偏移量(以 UTC 偏移量的形式表示时区)。
- 示例:2024-09-16T15:30:00+08:00
- 适用场景:用于需要 UTC 偏移量的场景,如服务器日志、国际通信等。
正则表达式
对于正则表达式a\&c来说,对应的Java字符串是"a\\&c",因为\也是Java字符串的转义字符,两个\\实际上表示的是一个\
如果想匹配非ASCII字符,例如中文,那就用\u####的十六进制表示,例如:a\u548cc匹配字符串"a和c",中文字符和的Unicode编码是548c。
用\s可以匹配一个空格字符,注意空格字符不但包括空格 ,还包括tab字符(在Java中用\t表示)。例如,a\sc可以匹配:
"a c",因为\s可以匹配空格字符 ;"a c",因为\s可以匹配tab字符\t。
用\d可以匹配一个数字,而\D则匹配一个非数字。例如,00\D可以匹配:
"00A",因为\D可以匹配非数字字符A;"00#",因为\D可以匹配非数字字符#。 00\d可以匹配的字符串"007","008"等,00\D是不能匹配的。 类似的,\W可以匹配\w不能匹配的字符,\S可以匹配\s不能匹配的字符,这几个正好是反着来的。
[...]还有一种排除法,即不包含指定范围的字符。假设我们要匹配任意字符,但不包括数字,可以写[^1-9]{3}
Pattern
Pattern p = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
- Pattern.compile(): 这是 Java 中编译正则表达式的方法,返回一个 Pattern 对象,用来表示某个正则表达式。
|
|
m.group(1) 和 m.group(2):这两个方法用于获取正则表达式中被捕获的子字符串。(以正则的括号分组)
非贪婪匹配
因为正则表达式默认使用贪婪匹配:任何一个规则,它总是尽可能多地向后匹配,因此,\d+总是会把后面的0包含进来。
要让\d+尽量少匹配,让0尽量多匹配,我们就必须让\d+使用非贪婪匹配。在规则\d+后面加个?即可表示非贪婪匹配。
反向引用
如果我们要把搜索到的指定字符串按规则替换,比如前后各加一个xxxx,这个时候,使用replaceAll()的时候,我们传入的第二个参数可以使用$1、$2来反向引用匹配到的子串。例如:
|
|
base64编码
Base64 编码是一种用于将二进制数据表示为文本字符串的编码方式。它将二进制数据转换成一种只包含 64 个字符(包括字母、数字和符号)的字符串,每 6 位被转换成一个对应的 Base64 字符。
|
|
多线程
操作系统调度的最小任务单位其实不是进程,而是线程。
方法1:
|
|
方法2:
|
|
一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行
JAVA多线程实现方式主要有三种: 1、继承Thread类 2、 实现Runnable接口 3、使用ExecutorService、Callable、Future实现有返回结果的多线程。 其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。
volatile
volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。 volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
守护线程
守护线程(Daemon Thread)。 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。 因此,JVM退出时,不必关心守护线程是否已结束。 如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:
|
|
synchronized
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:
|
|
synchronized 是一个由以下几个英文单词或词根组成的复合词:
- syn-:来自希腊语的前缀,表示 “一起”、“同步” 或 “联合”(意思是 together 或 with)。在编程中,它表示多个线程或进程同步进行。
- chron-:来源于希腊语 chronos,表示 “时间”(time)。它与时间相关,通常用于指与时间有关的操作。
- -ized:是一个后缀,用于构成动词,表示 “使成为…” 或 “…化”(to make something into a certain state)。
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)
当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:
|
|
可重入锁
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。 由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
避免死锁的方法是多线程获取锁的顺序要一致。
等待、通知
wait和notify用于多线程协调运行:
- 在synchronized内部可以调用wait()使线程进入等待状态;
- 必须在已获得的锁对象上调用wait()方法;
- 在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
- 必须在已获得的锁对象上调用notify()或notifyAll()方法;
- 已唤醒的线程还需要重新获得锁后才能继续执行。
ReentrantLock
ReentrantLock可以替代synchronized进行同步; ReentrantLock获取锁更安全; 必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁; 可以使用tryLock()尝试获取锁。
配合ReentrantLock,Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:
- await()会释放当前锁,进入等待状态;
- signal()会唤醒某个等待线程;
- signalAll()会唤醒所有等待线程;
- 唤醒线程从await()返回后需要重新获得锁。
ReadWriteLock
使用ReadWriteLock可以提高读取效率:
- ReadWriteLock只允许一个线程写入;
- ReadWriteLock允许多个线程在没有写入时同时读取;
- ReadWriteLock适合读多写少的场景。
StampedLock
StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能; StampedLock是不可重入锁。 和ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
Semaphore
如果要对某一受限资源进行限流访问,可以使用Semaphore,保证同一时间最多N个线程访问受限资源。 semaphore.acquire(); semaphore.release();
线程安全的集合
我们不必自己编写,可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合 interface non-thread-safe thread-safe List ArrayList CopyOnWriteArrayList Map HashMap ConcurrentHashMap Set HashSet / TreeSet CopyOnWriteArraySet Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue Deque ArrayDeque / LinkedList LinkedBlockingDeque
Java的java.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。
Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下: // 创建固定大小的线程池: ExecutorService executor = Executors.newFixedThreadPool(3);
java.util.concurrent 包
java.util.concurrent 包是 Java 并发编程的核心工具,提供了强大的工具来解决多线程环境下的同步、通信和任务执行问题。常见的用法包括:
- 线程池管理(ExecutorService)
- 异步任务(Future 和 Callable)
- 同步机制(如 CountDownLatch、Semaphore 等)
- 高效的线程安全集合类(如 ConcurrentHashMap)
- 原子操作类(如 AtomicInteger)
Future 和 Callable
Future 和 Callable
|
|
从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。 CompletableFuture可以指定异步处理流程:
- thenAccept()处理正常结果;
- exceptional()处理异常结果;
- thenApplyAsync()用于串行化另一个CompletableFuture;
- anyOf()和allOf()用于并行化多个CompletableFuture。
Fork/Join
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。 ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
ThreadLocal
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的; ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递); 使用ThreadLocal要用try … finally结构,并在finally中清除。
Maven
Maven 是一个强大的构建工具,它的核心功能包括 依赖管理 和 项目构建。虽然 Maven 确实可以自动从中央仓库或其他远程仓库下载 pom.xml 中指定的依赖包,但它的功能不仅仅限于此。Maven 主要的目标之一是帮助管理整个项目的构建生命周期,因此包括了编译、测试、打包、部署等功能。
对于某个依赖,Maven只需要3个变量即可唯一确定某个jar包:
- groupId:属于组织的名称,类似Java的包名;
- artifactId:该jar包自身的名称,类似Java的类名;
- version:该jar包的版本。
phase
使用mvn这个命令时,后面的参数是phase,Maven自动根据生命周期运行到指定的phase
经常用到的phase其实只有几个:
-
clean:清理
-
compile:编译
-
test:运行测试
-
package:打包
-
lifecycle相当于Java的package,它包含一个或多个phase;
-
phase相当于Java的class,它包含一个或多个goal;
-
goal相当于class的method,它其实才是真正干活的。 大多数情况,我们只要指定phase,就默认执行这些phase默认绑定的goal,只有少数情况,我们可以直接指定运行一个goal,例如,启动Tomcat服务器: $ mvn tomcat:run 执行每个phase,都是通过某个插件(plugin)来执行的,Maven本身其实并不知道如何执行compile,它只是负责找到对应的compiler插件,然后执行默认的compiler:compile这个goal来完成编译。
Maven已经内置了一些常用的标准插件: 插件名称 对应执行的phase clean clean compiler compile surefire test jar package
mvn compile:编译项目的源代码,生成 .class 文件,放在 target/classes 目录中。 mvn package:将编译后的代码和资源文件打包成 JAR、WAR 或其他格式的文件,生成的打包文件放在 target 目录中。
执行 mvn clean 和 mvn package 是一种确保 Maven 项目从干净状态重新构建的有效方法,可以解决编译依赖相关的错误。这样做可以清理缓存、更新依赖,并确保所有构建步骤都被正确执行。
多模块
在编译的时候,需要在根目录创建一个pom.xml统一编译:
<module>,一次性全部编译。
网路
理论知识
IPv4采用32位地址,类似101.202.99.12,而IPv6采用128位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428。IPv4地址总共有232个(大约42亿),而IPv6地址则总共有2128个(大约340万亿亿亿亿),IPv4的地址目前已耗尽,而IPv6的地址是根本用不完的。
如果两台计算机计算出的网络号不同,那么两台计算机不在同一个网络,不能直接通信,它们之间必须通过路由器或者交换机这样的网络设备间接通信,我们把这种设备称为网关。网关的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫路由。
OSI TCP/IP 应用层 应用层 表示层 应用层 会话层 应用层 传输层 传输层 网络层 IP层 链路层 网络接口层 物理层 网络接口层
IP协议是一个分组交换,它不保证可靠传输。而TCP协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP协议是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开。
注意到代码ss.accept()表示每当有新的客户端连接进来后,就返回一个Socket实例,这个Socket实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。
TCP vs UDP
使用Java进行TCP编程时,需要使用Socket模型:
- 服务器端用ServerSocket监听指定端口;
- 客户端使用Socket(InetAddress, port)连接服务器;
- 服务器端用accept()接收连接并返回Socket;
- 双方通过Socket打开InputStream/OutputStream读写数据;
- 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
- flush()用于强制输出缓冲区到网络。
使用UDP协议通信时,服务器和客户端双方无需建立连接:
- 服务器端用DatagramSocket(port)监听端口;
- 客户端使用DatagramSocket.connect()指定远程地址和端口;
- 双方通过receive()和send()读写数据;
- DatagramSocket没有IO流接口,数据被直接写入
byte[]缓冲区。
发送邮件的协议就是SMTP协议,它是Simple Mail Transport Protocol的缩写 接收邮件使用最广泛的协议是POP3:Post Office Protocol version 3,它也是一个建立在TCP连接之上的协议。另一种接收邮件的协议是IMAP:Internet Mail Access Protocol
常用服务端口号
服务类型 默认端口号 FTP(File Transfer Protocol) 21 (控制端口), 20 (数据端口) SSH(Secure Shell) 22 Telnet 23 SMTP(Simple Mail Transfer Protocol) 25 DNS(Domain Name System) 53 HTTP(Hypertext Transfer Protocol) 80 HTTPS(HTTP Secure) 443 POP3(Post Office Protocol version 3) 110 IMAP(Internet Message Access Protocol) 143 IMAPS(IMAP Secure) 993 POP3S(POP3 Secure) 995 MySQL 3306 PostgreSQL 5432 RDP(Remote Desktop Protocol) 3389 LDAP(Lightweight Directory Access Protocol) 389 LDAPS(LDAP Secure) 636 NTP(Network Time Protocol) 123 SNMP(Simple Network Management Protocol) 161 (普通), 162 (陷阱) HTTP/2 80 (HTTP), 443 (HTTPS) SMB(Server Message Block) 445 SFTP(SSH File Transfer Protocol) 22 (通过 SSH) Syslog 514
HTTP请求和响应
HTTP请求的格式是固定的,它由HTTP Header和HTTP Body两部分构成。第一行总是请求方法 路径 HTTP版本,例如,GET / HTTP/1.1表示使用GET请求,路径是/,版本是HTTP/1.1。 后续的每一行都是固定的Header: Value格式,我们称为HTTP Header,服务器依靠某些特定的Header来识别客户端请求,例如:
- Host:表示请求的域名,因为一台服务器上可能有多个网站,因此有必要依靠Host来识别请求是发给哪个网站的;
- User-Agent:表示客户端自身标识信息,不同的浏览器有不同的标识,服务器依靠User-Agent判断客户端类型是IE还是Chrome,是Firefox还是一个Python爬虫;
- Accept:表示客户端能处理的HTTP响应格式,
*/*表示任意格式,text/*表示任意文本,image/png表示PNG格式的图片; - Accept-Language:表示客户端接收的语言,多种语言按优先级排序,服务器依靠该字段给用户返回特定语言的网页版本。 如果是GET请求,那么该HTTP请求只有HTTP Header,没有HTTP Body。如果是POST请求,那么该HTTP请求带有Body,以一个空行分隔。
响应代码
HTTP有固定的响应代码:
- 1xx:表示一个提示性响应,例如101表示将切换协议,常见于WebSocket连接;
- 2xx:表示一个成功的响应,例如200表示成功,206表示只发送了部分内容;
- 3xx:表示一个重定向的响应,例如301表示永久重定向,303表示客户端应该按指定路径重新发送请求;
- 4xx:表示一个因为客户端问题导致的错误响应,例如400表示因为Content-Type等各种原因导致的无效请求,404表示指定的路径不存在;
- 5xx:表示一个因为服务器问题导致的错误响应,例如500表示服务器内部故障,503表示服务器暂时无法响应。
状态码 含义 描述 200 OK 请求成功,服务器返回所请求的数据。 201 Created 请求成功,服务器创建了新的资源。 204 No Content 请求成功,但服务器没有返回内容。 301 Moved Permanently 请求的资源已被永久移动到新位置。 302 Found 请求的资源临时移动到新位置。 304 Not Modified 请求的资源未被修改,自上次请求以来没有变化。 400 Bad Request 请求无效,服务器无法理解请求。 401 Unauthorized 请求未经授权,需要用户认证。 403 Forbidden 服务器拒绝执行请求,用户没有足够的权限。 404 Not Found 请求的资源在服务器上未找到。 405 Method Not Allowed 请求使用的 HTTP 方法被禁止。 408 Request Timeout 请求超时,服务器等待客户端请求超时。 409 Conflict 请求冲突,导致无法完成请求。 410 Gone 请求的资源已永久删除,不再可用。 415 Unsupported Media Type 请求的媒体类型不受支持。 429 Too Many Requests 客户端发送的请求过多,超出服务器的处理能力。 500 Internal Server Error 服务器内部错误,无法完成请求。 501 Not Implemented 服务器不支持请求的方法或功能。 502 Bad Gateway 服务器作为网关或代理时收到无效响应。 503 Service Unavailable 服务器当前无法处理请求,通常是由于过载或维护。 504 Gateway Timeout 服务器作为网关或代理时,等待上游服务器响应超时。
HTTP/1.1协议允许在一个TCP连接中反复发送-响应 HTTP/2.0允许客户端在没有收到响应的时候,发送多个HTTP请求,服务器返回响应的时候,不一定按顺序返回,只要双方能识别出哪个响应对应哪个请求,就可以做到并行发送和接收
XML/JSON
XML
XML是可扩展标记语言(eXtensible Markup Language)的缩写 字符 表示
|
|
因为XML是一种树形结构的文档,它有两种标准的解析API和jackson:
- DOM(Document Object Model):一次性读取XML,并在内存中表示为树形结构;
- SAX(Simple API for XML):以流的形式读取XML,使用事件回调。
- 一个名叫Jackson的开源的第三方库可以轻松做到XML到JavaBean的转换
JSON
JSON是JavaScript Object Notation的缩写,它去除了所有JavaScript执行代码,只保留JavaScript的对象格式。 JSON作为数据传输的格式,有几个显著的优点:
- JSON只允许使用UTF-8编码,不存在编码问题;
- JSON只允许使用双引号作为key,特殊字符用\转义,格式简单;
- 浏览器内置JSON支持,如果把数据用JSON发送给浏览器,可以用JavaScript直接处理。 所以,开发Web应用的时候,使用JSON作为数据传输,在浏览器端非常方便。因为JSON天生适合JavaScript处理,所以,绝大多数REST API都选择JSON作为数据传输格式。
在 JavaScript 中,JSON.parse() 和 JSON.stringify() 是两个用于处理 JSON 数据的重要方法。它们分别用于将 JSON 字符串转换为 JavaScript 对象,以及将 JavaScript 对象转换为 JSON 字符串。
Jackson也可以json to javabean 把JSON解析为JavaBean的过程称为反序列化。如果把JavaBean变为JSON,那就是序列化。
注意 json的序列化和java原生的序列化不同: Java 原生序列化和反序列化
- Java 原生序列化:Java 提供了内置的序列化机制,允许将 Java 对象转换为字节流(byte[]),并将其存储到文件、数据库或通过网络传输。这是通过实现 Serializable 接口来完成的。
- Java 原生反序列化:将存储的字节流重新转换为 Java 对象。反序列化恢复对象的状态,使得可以从存储或传输中恢复原始对象。
JDBC
PreparedStatement
使用PreparedStatement可以完全避免SQL注入的问题,因为PreparedStatement始终使用?作为占位符 ResultSet获取列时,索引从1开始而不是0;
一是调用prepareStatement()时,第二个参数必须传入常量Statement.RETURN_GENERATED_KEYS,否则JDBC驱动不会返回自增主键; 二是执行executeUpdate()方法后,必须调用getGeneratedKeys()获取一个ResultSet对象,这个对象包含了数据库自动生成的主键的值,读取该对象的每一行来获取自增主键的值。如果一次插入多条记录,那么这个ResultSet对象就会有多行返回值。如果插入时有多列自增,那么ResultSet对象的每一行都会对应多个自增值(自增列不一定必须是主键)。
特点
使用JDBC执行INSERT、UPDATE和DELETE都可视为更新操作;
数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
批量执行
|
|
常用的JDBC连接池有:
- HikariCP
- C3P0
- BoneCP
- Druid
函数式编程
函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算
|
|
流 Stream
Stream的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。 Stream的另一个特点是,一个Stream可以轻易地转换为另一个Stream,而不是修改原Stream本身。 最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。
这些操作对Stream来说可以分为两类,一类是转换操作,即把一个Stream转换为另一个Stream,例如map()和filter(),另一类是聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()。 区分这两种操作是非常重要的,因为对于Stream来说,对其进行转换操作并不会触发任何计算!
设计模式
类别 设计模式 描述 示例用途 创建型模式 单例模式 (Singleton) 确保一个类只有一个实例,并提供全局访问点。 数据库连接池、日志记录器 工厂方法模式 (Factory Method) 定义一个接口用于创建对象,但由子类决定实例化哪个类。 图形库中的图形创建 抽象工厂模式 (Abstract Factory) 提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定具体类。 用户界面主题(Windows 和 Mac 风格) 建造者模式 (Builder) 使用多个简单的对象一步步构建一个复杂的对象。 复杂对象的构建(如文档生成) 原型模式 (Prototype) 通过复制现有的实例来创建新实例,而不是通过构造函数。 克隆对象(如图形的复制) 结构型模式 适配器模式 (Adapter) 将一个类的接口转换成客户期望的另一个接口,使得原本不兼容的接口能够一起工作。 旧系统与新系统的接口兼容 装饰器模式 (Decorator) 动态地给对象添加额外的职责,而不影响其他对象。 GUI 组件的动态添加功能 桥接模式 (Bridge) 将抽象部分与实现部分分离,使它们可以独立变化。 图形库的实现与图形抽象分离 组合模式 (Composite) 允许你将对象组合成树形结构来表示“部分-整体”层次结构。 文件系统中的文件和文件夹 外观模式 (Facade) 提供一个统一的接口来访问子系统中的一组接口,从而简化复杂系统的使用。 复杂子系统的简化访问 享元模式 (Flyweight) 使用共享对象来支持大量的细粒度的对象。 文本编辑器中的字符对象 行为型模式 策略模式 (Strategy) 定义一系列算法,并使它们可以互相替换,使算法的变化不会影响到使用算法的客户。 购物车中的优惠策略 观察者模式 (Observer) 定义对象之间的一对多依赖,使得当一个对象改变状态时,所有依赖于它的对象都会得到通知并自动更新。 GUI 事件处理 状态模式 (State) 允许对象在其内部状态改变时改变它的行为。 状态机(如工作流引擎) 责任链模式 (Chain of Responsibility) 使多个对象都有机会处理请求,避免请求发送者与接收者之间的耦合。 事件处理链 命令模式 (Command) 将请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,排队请求,记录请求日志,支持撤销操作。 图形应用中的操作和撤销操作 解释器模式 (Interpreter) 给定一种语言,定义它的文法,并定义一个解释器来解释该语言中的句子。 解析和执行简单语言(如正则表达式解析) 迭代器模式 (Iterator) 提供一种方法顺序访问集合对象中的元素,而不暴露集合的内部表示。 遍历集合对象 模板方法模式 (Template Method) 定义一个操作的算法的骨架,而将一些步骤延迟到子类中。 数据处理的标准流程 中介者模式 (Mediator) 用一个中介对象封装一系列的对象交互,使得这些对象不需要显式地引用对方,从而使它们耦合松散。 GUI 控件之间的交互 备忘录模式 (Memento) 在不暴露对象内部状态的情况下,捕获和外部化对象的内部状态,以便以后可以恢复到该状态。 游戏中的撤销操作 访问者模式 (Visitor) 表示一个作用于对象结构中的各个元素的操作,允许在不改变这些元素的类的前提下定义作用于这些元素的新操作。 处理不同类型元素的操作
Servlet
最早的JavaEE的名称是J2EE:Java 2 Platform Enterprise Edition,后来改名为JavaEE。
一个Servlet总是继承自HttpServlet,然后覆写doGet()或doPost()方法。注意到doGet()方法传入了HttpServletRequest和HttpServletResponse两个对象,分别代表HTTP请求和响应。
要运行我们的hello.war,首先要下载Tomcat服务器,解压后,把hello.war复制到Tomcat的webapps目录下,然后切换到bin目录,执行startup.sh或startup.bat启动Tomcat服务器。因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。
使用重定向时,浏览器知道重定向规则,并且会自动发起新的HTTP请求; 使用转发时,浏览器并不知道服务器内部的转发逻辑。
HttpSession
Servlet提供的HttpSession本质上就是通过一个名为JSESSIONID的Cookie来跟踪用户会话的。除了这个名称外,其他名称的Cookie我们可以任意使用。
JSP
整个JSP的内容实际上是一个HTML,但是稍有不同:
- 包含在<%–和–%>之间的是JSP的注释,它们会被完全忽略;
- 包含在<%和%>之间的是Java代码,可以编写任意Java代码;
- 如果使用<%= xxx %>则可以快捷输出一个变量的值。
MVC模式
使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。
过滤器
在 Filter 中,chain.doFilter(request, response) 是一个关键方法,它确保请求能够传递到下一个过滤器或最终的目标资源(例如 Servlet、JSP 页面)。这句话的意思是:如果 Filter 没有调用 chain.doFilter(),请求的处理链就会中断,后续的过滤器或目标资源将不会执行。
Spring
IoC
在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在IoC容器中被“装配”出来,需要某种“注入”机制。IoC全称Inversion of Control,直译为控制反转。因此,IoC又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。
我们需要创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码:
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
接下来,我们就可以从Spring容器中“取出”装配好的Bean然后使用它:
// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");
Annotation配置
使用xml配置不方便,推荐使用Annotation配置 使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:
- 每个Bean被标注为@Component并正确使用@Autowired注入;
- 配置类被标注为@Configuration和@ComponentScan;
- 所有Bean均在指定包以及子包内。 使用@ComponentScan非常方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置AppConfig位于自定义的顶层包(例如com.itranswarp.learnjava),其他Bean按类别放入子包。
指定Scope为Prototype
Spring默认使用Singleton创建Bean,也可指定Scope为Prototype; 可将相同类型的Bean注入List或数组; 可用@Autowired(required=false)允许可选注入; 可用带@Bean标注的方法创建Bean; 可使用@PostConstruct和@PreDestroy对Bean进行初始化和清理; 相同类型的Bean只能有一个指定为@Primary,其他必须用@Qualifier(“beanName”)指定别名; 注入时,可通过别名@Qualifier(“beanName”)指定某个Bean; 可以定义FactoryBean来使用工厂模式创建Bean。
注入
|
|
Spring容器看到@PropertySource(“app.properties”)注解后,自动读取这个配置文件,然后,我们使用@Value正常注入
另一种注入配置的方式是先通过一个简单的JavaBean持有所有的配置。
然后,在需要读取的地方,使用#{smtpConfig.host}注入:
|
|
注意观察#{}这种注入语法,它和${key}不同的是,#{}表示从JavaBean读取属性。"#{smtpConfig.host}“的意思是,从名称为smtpConfig的Bean读取host属性,即调用getHost()方法。一个Class名为SmtpConfig的Bean,它在Spring容器中的默认名称就是smtpConfig,除非用@Qualifier指定了名称。
AOP
AOP是Aspect Oriented Programming,即面向切面编程。
一、使用AspectJ的注解使用AOP,一共需要三步:
- 定义执行方法,并在方法上通过AspectJ的注解告诉Spring应该在何处调用此方法;
- 标记@Component和@Aspect;
- 在@Configuration类上标注@EnableAspectJAutoProxy。
二、使用注解装配AOP