Java笔记
Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
Java快速入门
Java简介
- JDK:Java Development Kit
- JRE:Java Runtime Environment
简单地说,JRE就是运行Java字节码的虚拟机。但是,如果只有Java源码,要编译成Java字节码,就需要JDK,因为JDK除了包含JRE,还提供了编译器、调试器等开发工具。
二者关系如下:
1 |
|
Java程序基础
int[] ns = new int[5];
对象、数组都是引用数据类型。
流程控制
if判断
==
表示“引用的对象是否相等”,浮点数判断相等不能直接用
使用equals()
判断引用类型内容相等,注意避免NullPointerException
。
for each循环
for each
循环可以直接遍历数组的每个元素;
1 |
|
数组操作
遍历数组
遍历数组可以使用for
循环,for
循环可以访问数组索引,for each
循环直接迭代每个数组元素,但无法获取索引;
使用Arrays.toString()
可以快速获取数组内容。
排序
排序前
1 |
|
排序后,原来的3个字符串在内存中均没有任何变化,但是ns
数组的每个元素指向变化了。
1 |
|
面向对象编程
Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming,简称OOP。
面向对象基础
在OOP中,class
和instance
是“模版”和“实例”的关系;
方法
在方法内部,可以使用一个隐含的变量this
,它始终指向当前实例。因此,通过this.field
就可以访问当前实例的字段。
方法重载
方法重载是指多个方法的方法名相同,但各自的参数不同;
重载方法返回值类型应该相同。
继承
区分继承和组合
Student
和Book
的关系是has关系。
- 继承是面向对象编程的一种强大的代码复用方式;
- Java只允许单继承,所有类最终的根类是
Object
; protected
允许子类访问父类的字段和方法;- 子类的构造方法可以通过
super()
调用父类的构造方法; - 可以安全地向上转型为更抽象的类型;
- 可以强制向下转型,最好借助
instanceof
判断; - 子类和父类的关系是is,has关系不能用继承。
多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。
- 子类可以覆写父类的方法(Override),覆写在子类中改变了父类方法的行为;
- Java的方法调用总是作用于运行期对象的实际类型,这种行为称为多态;
final
修饰符有多种作用:final
修饰的方法可以阻止被覆写;final
修饰的class可以阻止被继承;final
修饰的field必须在创建对象时初始化,随后不可修改。
抽象类
通过abstract
定义的方法是抽象方法,它只有定义,没有实现。抽象方法定义了子类必须实现的接口规范;
接口
所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为不能定义实例字段。
interface
的字段只能是public static final
类型
静态字段和静态方法
所有实例共享一个静态字段。
不推荐用实例变量.静态字段
去访问静态字段,推荐用类名来访问静态字段。
包
Java内建的package
机制是为了避免class
命名冲突;
JDK的核心类使用java.lang
包,编译器会自动导入;
JDK的其它常用类定义在java.util.*
,java.math.*
,java.text.*
,……;
包名推荐使用倒置的域名,避免冲突,例如org.apache
。
包没有父子关系,com.apache
和com.apache.abc
是不同的包。
作用域
定义在一个class
内部的class
称为嵌套类(nested class
)
如果不确定是否需要public
,就不声明为public
,即尽可能少地暴露对外的字段和方法。
把方法定义为package
权限有助于测试,因为测试类和被测试类只要位于同一个package
,测试代码就可以访问被测试类的package
权限方法。
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同。
protected
作用于继承关系。定义为protected
的字段和方法可以被子类访问,以及子类的子类
classpath和jar
classpath
是JVM用到的一个环境变量,它用来指示JVM如何搜索class
。
因为Java是编译型语言,源码文件是.java
,而编译后的.class
文件才是真正可以被JVM执行的字节码。
jar包实际上就是一个zip格式的压缩文件,而jar包相当于目录。
如何创建jar包?
因为jar包就是zip包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择“发送到”,“压缩(zipped)文件夹”,就制作了一个zip文件。然后,把后缀从.zip
改为.jar
,一个jar包就创建成功。
小结
JVM通过环境变量classpath
决定搜索class
的路径和顺序;
不推荐设置系统环境变量classpath
,始终建议通过-cp
命令传入;
jar包相当于目录,可以包含很多.class
文件,方便下载和使用;
MANIFEST.MF
文件可以提供jar包的信息,如Main-Class
,这样可以直接运行jar包。
Java核心类
字符串和编码
- Java字符串
String
是不可变对象; - 字符串操作不改变原字符串内容,而是返回新字符串;
- 常用的字符串操作:提取子串、查找、替换、大小写转换等;
- Java使用Unicode编码表示
String
和char
; - 转换编码就是将
String
和byte[]
转换,需要指定编码; - 转换为
byte[]
时,始终优先考虑UTF-8
编码。
StringBuilder
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象;
StringBuilder
可以支持链式操作,实现链式操作的关键是返回实例本身;
StringJoiner
用指定分隔符拼接字符串数组时,使用StringJoiner
或者String.join()
更方便;
用StringJoiner
拼接字符串时,还可以额外附加一个“开头”和“结尾”。
包装类型
Java核心库提供的包装类型可以把基本类型包装为class
;
自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5);
装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException
;
包装类型的比较必须使用equals()
;
整数和浮点数的包装类型都继承自Number
;
包装类型提供了大量实用方法。
JavaBean
JavaBean是一种符合命名规范的class
,它通过getter
和setter
来定义属性;
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输;
可以利用IDE快速生成getter
和setter
;
属性是一种通用的叫法,并非Java语法规定;
使用Introspector.getBeanInfo()
可以获取属性列表。
枚举类
Java使用enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
;
通过name()
获取常量定义的字符串,注意不要使用toString()
;
通过ordinal()
返回常量定义的顺序(无实质意义);
可以为enum
编写构造方法、字段和方法
enum
的构造方法要声明为private
,字段强烈建议声明为final
;
enum
适合用在switch
语句中。
1 |
|
纪录类
使用String
、Integer
等类型的时候,这些类型都是不变类,一个不变类具有以下特点:
- 定义class时使用
final
,无法派生子类; - 每个字段使用
final
,保证创建实例后无法修改任何字段。
public record Point(int x, int y) {}
从Java 14开始,提供新的record
关键字,可以非常方便地定义Data Class:
- 使用
record
定义的是不变类; - 可以编写Compact Constructor对参数进行验证;
- 可以定义静态方法。
BigInteger
BigInteger
用于表示任意大小的整数;
BigInteger
是不变类,并且继承自Number
;
将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。
BigDecimal
如果查看BigDecimal
的源码,可以发现,实际上一个BigDecimal
是通过一个BigInteger
和一个scale
来表示的,即BigInteger
表示一个完整的整数,而scale
表示小数位数;
BigDecimal
用于表示精确的小数,常用于财务计算;
比较BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
常用工具类
Math
数学计算
Random
创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
SecureRandom
安全的随机数,真随机数。种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
异常处理
Java的异常
Java使用异常来表示错误,并通过try ... catch
捕获异常;
Java的异常是class
,并且从Throwable
继承;
Error
是无需捕获的严重错误,Exception
是应该捕获的可处理的错误;
RuntimeException
无需强制捕获,非RuntimeException
(Checked Exception)需强制捕获,或者用throws
声明;
如果不想写任何try
代码,可以直接把main()
方法定义为throws Exception
。也就声明了可能抛出所有的Exception
,因此在内部就无需捕获了。代价就是一旦发生异常,程序会立刻退出。
捕获异常
使用try ... catch ... finally
时:
- 多个
catch
语句的匹配顺序非常重要,子类必须放在前面; finally
语句保证了有无异常都会执行,它是可选的;- 一个
catch
语句也可以匹配多个非继承关系的异常。
抛出异常
调用printStackTrace()
可以打印异常的传播栈,对于调试非常有用;
捕获异常并再次抛出新的异常时,应该持有原始异常信息;
通常不要在finally
中抛出异常。如果在finally
中抛出异常,应该原始异常加入到原有异常中。调用方可通过Throwable.getSuppressed()
获取所有添加的Suppressed Exception
。
自定义异常
抛出异常时,尽量复用JDK已定义的异常类型;
自定义异常体系时,推荐从RuntimeException
派生“根异常”,再派生出业务异常;
自定义异常时,应该提供多种构造方法。
NullPointerException
空指针异常
编写业务逻辑时,用空字符串""
表示未填写比null
安全得多。
断言
断言(Assertion)是一种调试程序的方式。在Java中,使用assert
关键字来实现断言。
JDK Logging
日志是为了替代System.out.println()
,可以定义格式,重定向到文件等;
日志可以存档,便于追踪问题;
日志记录可以按级别分类,便于打开或关闭某些级别;
可以根据配置文件调整日志,无需修改代码;
Java标准库提供了java.util.logging
来实现日志功能。
Commons Logging
和Java标准库提供的日志不同,Commons Logging是一个第三方日志库,它是由Apache创建的日志模块。
Commons Logging是使用最广泛的日志模块;
Commons Logging的API非常简单;
Commons Logging可以自动检测并使用其他日志模块。
反射
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
Class类
JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息;
获取一个class对应的Class实例后,就可以获取该class的所有信息;
通过Class实例获取class信息的方法称为反射(Reflection);
JVM总是动态加载class,可以在运行期根据条件来控制加载class。
访问字段
Java的反射API提供的Field
类封装了字段的所有信息:
通过Class
实例的方法可以获取Field
实例:getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
;
通过Field实例可以获取字段信息:getName()
,getType()
,getModifiers()
;
通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)
来访问非public
字段。
通过反射读写字段是一种非常规方法,它会破坏对象的封装。
调用方法
Java的反射API提供的Method对象封装了方法的所有信息:
通过Class
实例的方法可以获取Method
实例:getMethod()
,getMethods()
,getDeclaredMethod()
,getDeclaredMethods()
;
通过Method
实例可以获取方法信息:getName()
,getReturnType()
,getParameterTypes()
,getModifiers()
;
通过Method
实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
;
通过设置setAccessible(true)
来访问非public
方法;
通过反射调用方法时,仍然遵循多态原则。
调用构造方法
Constructor
对象封装了构造方法的所有信息;
通过Class
实例的方法可以获取Constructor
实例:getConstructor()
,getConstructors()
,getDeclaredConstructor()
,getDeclaredConstructors()
;
通过Constructor
实例可以创建一个实例对象:newInstance(Object... parameters)
; 通过设置setAccessible(true)
来访问非public
构造方法。
获取继承关系
通过Class
对象可以获取继承关系:
Class getSuperclass()
:获取父类类型;Class[] getInterfaces()
:获取当前类实现的所有接口。
通过Class
对象的isAssignableFrom()
方法可以判断一个向上转型是否可以实现。
动态代理
Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
动态代理是通过Proxy
创建代理对象,然后将接口方法“代理”给InvocationHandler
完成的。
注解
使用注解
注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”。
注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。
注解(Annotation)是Java语言用于工具处理的标注:
注解可以配置参数,没有指定配置的参数使用默认值;
如果参数名称是value
,且只有一个参数,那么可以省略参数名称。
定义注解
Java使用@interface
定义注解:
可定义多个参数和默认值,核心参数使用value
名称;
必须设置@Target
来指定Annotation
可以应用的范围;
应当设置@Retention(RetentionPolicy.RUNTIME)
便于运行期读取该Annotation
。
处理注解
可以在运行期通过反射读取RUNTIME
类型的注解,注意千万不要漏写@Retention(RetentionPolicy.RUNTIME)
,否则运行期无法读取到该注解。
可以通过程序处理注解来实现相应的功能:
- 对JavaBean的属性值按规则进行检查;
- JUnit会自动运行
@Test
标记的测试方法。
泛型
集合
Java的集合类定义在java.util
包中,支持泛型,主要提供了3种集合类,包括List
,Set
和Map
。Java集合使用统一的Iterator
遍历,尽量不要使用遗留接口。
List
List
的行为和数组几乎完全相同。 List是一个接口,而ArrayList是List接口的一个实现类。
List
是按索引顺序访问的长度可变的有序表,优先使用ArrayList
而不是LinkedList
;
1 |
|
通过Iterator
遍历List
永远是最高效的方式。并且,由于Iterator
遍历是如此常用,所以,Java的for each
循环本身就可以帮我们使用Iterator
遍历。
List
可以和Array
相互转换。
List和Array的区别是什么?
- 数组是定长,list是自动增长。
- 数组效率高,list效率低。
编写equals方法
List
还提供了boolean contains(Object o)
方法来判断List
是否包含某个指定元素。此外,int indexOf(Object o)
方法可以返回某个元素的索引,如果元素不存在,就返回-1
。
对于引用字段比较,我们使用equals()
,对于基本类型字段的比较,我们使用==
。
总结一下equals()
方法的正确编写方法:
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof
判断传入的待比较的Object
是不是当前类型,如果是,继续比较,否则,返回false
; - 对引用类型用
Objects.equals()
比较,对基本类型直接用==
比较。
如果不调用List
的contains()
、indexOf()
这些方法,就不必覆写equals()
方法。
Map
Map
是一种映射表,可以通过key
快速查找value
。无序。
可以通过for each
遍历keySet()
,也可以通过for each
遍历entrySet()
,直接获取key-value
。
1 |
|
最常用的一种Map
实现是HashMap
。
编写equals和hashCode
HashMap
之所以能根据key
直接拿到value
,原因是它内部通过空间换时间的方法,用一个大数组存储所有value
,并根据key直接计算出value
应该存储在哪个索引
要正确使用HashMap
,作为key
的类必须正确覆写equals()
和hashCode()
方法;
一个类如果覆写了equals()
,就必须覆写hashCode()
,并且覆写规则是:
- 如果
equals()
返回true
,则hashCode()
返回值必须相等; - 如果
equals()
返回false
,则hashCode()
返回值尽量不要相等。
实现hashCode()
方法可以通过Objects.hashCode()
辅助方法实现。
EnumMap
如果Map
的key是enum
类型,推荐使用EnumMap
,既保证速度,也不浪费空间。
使用EnumMap
的时候,根据面向抽象编程的原则,应持有Map
接口。
TreeMap
1 |
|
SortedMap
是接口,它的实现类是TreeMap
。创建TreeMap
时同时指定一个自定义排序算法
1 |
|
SortedMap
在遍历时严格按照Key的顺序遍历,最常用的实现类是TreeMap
;
作为SortedMap
的Key必须实现Comparable
接口,或者传入Comparator
;
要严格按照compare()
规范实现比较逻辑,否则,TreeMap
将不能正常工作。
Properties
Java集合库提供的Properties
用于读写配置文件.properties
。.properties
文件可以使用UTF-8编码。
可以从文件系统、classpath或其他任何地方读取.properties
文件。
读写Properties
时,注意仅使用getProperty()
和setProperty()
方法,不要调用继承而来的get()
和put()
等方法。
Set
Set
实际上相当于只存储key、不存储value的Map
。
Set
用于存储不重复的元素集合:
- 放入
HashSet
的元素与作为HashMap
的key要求相同; - 放入
TreeSet
的元素与作为TreeMap
的Key要求相同;
利用Set
可以去除重复元素;
遍历SortedSet
按照元素的排序顺序遍历,也可以自定义排序算法。
Queue
队列Queue
实现了一个先进先出(FIFO)的数据结构:
- 通过
add()
/offer()
方法将元素添加到队尾; - 通过
remove()
/poll()
从队首获取元素并删除; - 通过
element()
/peek()
从队首获取元素但不删除。
要避免把null
添加到队列,很难确定是取到了null
元素还是队列为空。
PriorityQueue
PriorityQueue
实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素。
PriorityQueue
默认按元素比较的顺序排序(必须实现Comparable
接口),也可以通过Comparator
自定义排序算法(元素就不必实现Comparable
接口)。
Deque
Deque
实现了一个双端队列(Double Ended Queue),它可以:
- 将元素添加到队尾或队首:
addLast()
/offerLast()
/addFirst()
/offerFirst()
; - 从队首/队尾获取元素并删除:
removeFirst()
/pollFirst()
/removeLast()
/pollLast()
; - 从队首/队尾获取元素但不删除:
getFirst()
/peekFirst()
/getLast()
/peekLast()
; - 总是调用
xxxFirst()
/xxxLast()
以便与Queue
的方法区分开; - 避免把
null
添加到队列。
Deque
是一个接口,它的实现类有ArrayDeque
和LinkedList
。
Stack
栈(Stack)是一种后进先出(LIFO)的数据结构,操作栈的元素的方法有:
- 把元素压栈:
push(E)
; - 把栈顶的元素“弹出”:
pop(E)
; - 取栈顶元素但不弹出:
peek(E)
。
在Java中,我们用Deque
可以实现Stack
的功能,注意只调用push()
/pop()
/peek()
方法,避免调用Deque
的其他方法。
最后,不要使用遗留类Stack
。
Iterator
我们把这种通过Iterator
对象遍历集合的模式称为迭代器。
使用迭代器的好处在于,调用方总是以统一的方式遍历各种集合类型,而不必关系它们内部的存储结构。
Iterator
是一种抽象的数据访问模型。使用Iterator
模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的
Iterator
对象知道如何迭代。
Java提供了标准的迭代器模型,即集合类实现java.util.Iterable
接口,返回java.util.Iterator
实例。
Collections
Collections
类提供了一组工具方法来方便使用集合类:
- 创建空集合;
- 创建单元素集合;
- 创建不可变集合;
- 排序/洗牌等操作。
IO
IO是指Input/Output,即输入和输出。以内存为中心:
- Input指从外部读入数据到内存
- Output指把数据从内存输出到外部
IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。
IO流以byte
(字节)为最小单位,因此也称为字节流。
如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char
来读写显然更方便,这种流称为字符流。
同步和异步
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
小结
IO流是一种流式的数据输入/输出模型:
- 字节流,二进制数据以
byte
为最小单位在InputStream
/OutputStream
中单向流动; - 字符流,字符数据以
char
为最小单位在Reader
/Writer
中单向流动。
File
传入相对路径时,相对路径前面加上当前目录就是绝对路径:
1 |
|
File
对象既可以表示文件,也可以表示目录。
Java标准库的java.io.File
对象表示一个文件或者目录:
- 创建
File
对象本身不涉及IO操作; - 可以获取路径/绝对路径/规范路径:
getPath()
/getAbsolutePath()
/getCanonicalPath()
; - 可以获取目录的文件和子目录:
list()
/listFiles()
; - 可以创建或删除文件和目录。
InputStream
用try ... finally
来保证InputStream
在无论是否发生IO错误的时候都能够正确地关闭
Java标准库的java.io.InputStream
定义了所有输入流的超类:
FileInputStream
实现了文件流输入;ByteArrayInputStream
在内存中模拟一个字节流输入。
总是使用try(resource)
来保证InputStream
正确关闭。
OutputStream
为什么要有flush()
?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]
数组),等到缓冲区写满了,再一次性写入文件或者网络。
Java标准库的java.io.OutputStream
定义了所有输出流的超类:
FileOutputStream
实现了文件流输出;ByteArrayOutputStream
在内存中模拟一个字节流输出。
某些情况下需要手动调用OutputStream
的flush()
方法来强制输出缓冲区。
总是使用try(resource)
来保证OutputStream
正确关闭。
Filter
Java的IO标准库使用Filter模式为InputStream
和OutputStream
增加功能:
- 可以把一个
InputStream
和任意个FilterInputStream
组合; - 可以把一个
OutputStream
和任意个FilterOutputStream
组合。
Filter模式可以在运行期动态增加功能(又称Decorator模式)。
操作Zip
ZipInputStream
可以读取zip格式的流,ZipOutputStream
可以把多份数据写入zip包;
配合FileInputStream
和FileOutputStream
就可以读写zip文件。
读取classpath资源
把资源存储在classpath中可以避免文件路径依赖;
Class
对象的getResourceAsStream()
可以从classpath中读取指定资源;
根据classpath读取资源时,需要检查返回的InputStream
是否为null
。
序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]
数组。
为什么要把Java对象序列化呢?因为序列化后可以把byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
可序列化的Java对象必须实现java.io.Serializable
接口,类似Serializable
这样的空接口被称为“标记接口”(Marker Interface);
反序列化时不调用构造方法,可设置serialVersionUID
作为版本号(非必需);
Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。
Reader
Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取。
Reader
定义了所有字符输入流的超类:
FileReader
实现了文件字符流输入,使用时需要指定编码;CharArrayReader
和StringReader
可以在内存中模拟一个字符流输入。
Reader
是基于InputStream
构造的:可以通过InputStreamReader
在指定编码的同时将任何InputStream
转换为Reader
。
总是使用try (resource)
保证Reader
正确关闭。
Writer
Writer
定义了所有字符输出流的超类:
FileWriter
实现了文件字符流输出;CharArrayWriter
和StringWriter
在内存中模拟一个字符流输出。
使用try (resource)
保证Writer
正确关闭。
Writer
是基于OutputStream
构造的,可以通过OutputStreamWriter
将OutputStream
转换为Writer
,转换时需要指定编码。
PrintStream和PrintWriter
PrintStream
是一种FilterOutputStream
,它在OutputStream
的接口上,额外提供了一些写入各种数据类型的方法。
PrintStream
是一种能接收各种数据类型的输出,打印数据时比较方便:
System.out
是标准输出;System.err
是标准错误输出。
PrintWriter
是基于Writer
的输出。
使用Files
对于简单的小文件读写操作,可以使用Files
工具类简化代码。
多线程
多线程基础
现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务。例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。
和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
Java多线程编程的特点又在于:
- 多线程模型是Java程序最基本的并发模型;
- 后续读写网络、数据库、Web开发等都依赖Java多线程模型。
创建新线程
Java用Thread
对象表示一个线程,通过调用start()
启动一个新线程;
一个线程对象只能调用一次start()
方法;
线程的执行代码写在run()
方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
Thread.sleep()
可以把当前线程暂停一段时间。
线程的状态
Java线程对象Thread
的状态包括:New
、Runnable
、Blocked
、Waiting
、Timed Waiting
和Terminated
;
通过对另一个线程对象调用join()
方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用join()
方法会立刻返回。
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。
对目标线程调用interrupt()
方法可以请求中断一个线程,目标线程通过检测isInterrupted()
标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
;
目标线程检测到isInterrupted()
为true
或者捕获了InterruptedException
都应该立刻结束自身线程;
public volatile boolean running = true;
通过标志位判断需要正确使用volatile
关键字;
volatile
关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。
守护线程
Java程序入口就是由JVM启动main
线程,main
线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程。如果这个线程不结束,JVM进程就无法结束。
守护线程(Daemon Thread)是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
守护线程不能持有需要关闭的资源(如打开文件等)。
线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized
同步;
同步的本质就是给指定对象加锁lock,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
同步方法
用synchronized
修饰方法可以把整个方法变为同步代码块,synchronized
方法加锁对象是this
;
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;
多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。
死锁
Java的synchronized
锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。
wait和notify
wait
和notify
用于多线程协调运行:
- 在
synchronized
内部可以调用wait()
使线程进入等待状态; - 必须在已获得的锁对象上调用
wait()
方法; - 在
synchronized
内部可以调用notify()
或notifyAll()
唤醒其他等待线程; - 必须在已获得的锁对象上调用
notify()
或notifyAll()
方法; - 已唤醒的线程还需要重新获得锁后才能继续执行。
ReentrantLock
ReentrantLock
可以替代synchronized
进行同步;
ReentrantLock
获取锁更安全;
必须先获取到锁,再进入try {...}
代码块,最后使用finally
保证释放锁;
可以使用tryLock()
尝试获取锁。
Condition
Condition
可以替代wait
和notify
;
Condition
对象必须从Lock
对象获取。
ReadWriteLock
使用ReadWriteLock
可以提高读取效率:
ReadWriteLock
只允许一个线程写入;ReadWriteLock
允许多个线程在没有写入时同时读取;ReadWriteLock
适合读多写少的场景。
StampedLock
StampedLock
和ReadWriteLock
相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
StampedLock
提供了乐观读锁,可取代ReadWriteLock
以进一步提升并发性能;
StampedLock
是不可重入锁。
Concurrent集合
使用java.util.concurrent
包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码。
Atomic
使用java.util.concurrent.atomic
提供的原子操作可以简化多线程编程:
- 原子操作实现了无锁的线程安全;
- 适用于计数器,累加器等。
线程池
JDK提供了ExecutorService
实现了线程池功能:
- 线程池内部维护一组线程,可以高效执行大量小任务;
Executors
提供了静态方法创建不同类型的ExecutorService
;- 必须调用
shutdown()
关闭ExecutorService
; ScheduledThreadPool
可以定期调度多个任务。
Future
对线程池提交一个Callable
任务,可以获得一个Future
对象;
可以用Future
在将来某个时刻获取结果。
CompletableFuture
CompletableFuture
可以指定异步处理流程:
thenAccept()
处理正常结果;exceptional()
处理异常结果;thenApplyAsync()
用于串行化另一个CompletableFuture
;anyOf()
和allOf()
用于并行化多个CompletableFuture
。
ForkJoin
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool
线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask
或RecursiveAction
。
使用Fork/Join模式可以进行并行计算以提高效率。
ThreadLocal
ThreadLocal
表示线程的“局部变量”,它确保每个线程的ThreadLocal
变量都是各自独立的;
ThreadLocal
适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal
要用try ... finally
结构,并在finally
中清除。
Maven基础
Maven是一个Java项目管理和构建工具,它可以定义项目结构、项目依赖,并使用统一的方式进行自动化构建。
Maven介绍
项目结构
1 |
|
一个Java项目的管理和构建工具:
- Maven使用
pom.xml
定义项目内容,并使用预设的目录结构; - 在Maven中声明一个依赖项可以自动下载并导入classpath;
- Maven使用
groupId
,artifactId
和version
唯一定位一个依赖。
依赖管理
Maven通过解析依赖关系确定项目所需的jar包,常用的4种scope
有:compile
(默认),test
,runtime
和provided
;
Maven从中央仓库下载所需的jar包并缓存在本地;
可以通过镜像仓库加速下载。
构建流程
Maven通过lifecycle、phase和goal来提供标准的构建流程。
最常用的构建命令是指定phase,然后让Maven执行到指定的phase:
- mvn clean
- mvn clean compile
- mvn clean test
- mvn clean package
通常情况,我们总是执行phase默认绑定的goal,因此不必指定goal。
使用插件
Maven通过自定义插件可以执行项目构建时需要的额外功能,使用自定义插件必须在pom.xml中声明插件及配置;
插件会在某个phase被执行时执行;
插件的配置和用法需参考插件的官方文档。
模块管理
Maven支持模块化管理,可以把一个大项目拆成几个模块:
- 可以通过继承在parent的
pom.xml
统一定义重复配置; - 可以通过
<modules>
编译多个模块。
mvnw
使用Maven Wrapper,可以为一个项目指定特定的Maven版本。
发布Artifact
使用Maven发布一个Artifact时:
- 可以发布到本地,然后由静态服务器提供repo服务,使用方必须声明repo地址;
- 可以发布到central.sonatype.org,并自动同步到Maven中央仓库,需要前期申请账号以及本地配置;
- 可以发布到GitHub Packages作为私有仓库使用,必须提供Token以及正确的权限才能发布和使用。
网络编程
网络编程基础
计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。
因为直接记忆IP地址非常困难,所以我们通常使用域名访问某个特定的服务。域名解析服务器DNS负责把域名翻译成对应的IP,客户端再根据IP地址访问服务器。
计算机网络的基本概念主要有:
- 计算机网络:由两台或更多计算机组成的网络;
- 互联网:连接网络的网络;
- IP地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
- 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
- 网络协议:互联网使用TCP/IP协议,它泛指互联网协议簇;
- IP协议:一种分组交换传输协议;
- TCP协议:传输控制协议,一种面向连接,可靠传输的协议;
- UDP协议:用户数据报协议,一种无连接,不可靠传输的协议
TCP编程
为什么需要Socket进行网络通信?
因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。
一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开。
使用Java进行TCP编程时,需要使用Socket模型:
- 服务器端用
ServerSocket
监听指定端口; - 客户端使用
Socket(InetAddress, port)
连接服务器; - 服务器端用
accept()
接收连接并返回Socket
; - 双方通过
Socket
打开InputStream
/OutputStream
读写数据; - 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
flush()
用于强制输出缓冲区到网络。
UDP编程
使用UDP协议通信时,服务器和客户端双方无需建立连接:
- 服务器端用
DatagramSocket(port)
监听端口; - 客户端使用
DatagramSocket.connect()
指定远程地址和端口; - 双方通过
receive()
和send()
读写数据; DatagramSocket
没有IO流接口,数据被直接写入byte[]
缓冲区。
发生Email
使用JavaMail API发送邮件本质上是一个MUA(Mail User Agent)软件通过SMTP协议发送邮件至MTA(Mail Transfer Agent)服务器;
打开调试模式可以看到详细的SMTP交互信息;
某些邮件服务商需要开启SMTP,并需要独立的SMTP登录密码。
接收Email
使用Java接收Email时,可以用POP3协议或IMAP协议。
使用POP3协议时,需要用Maven引入JavaMail依赖,并确定POP3服务器的域名/端口/是否使用SSL等,然后,调用相关API接收Email。
设置debug模式可以查看通信详细内容,便于排查错误。
HTTP编程
什么是HTTP?HTTP就是目前使用最广泛的Web应用程序使用的基础协议,例如,浏览器访问网站,手机App访问后台服务器,都是通过HTTP协议实现的。
Java提供了HttpClient
作为新的HTTP客户端编程接口用于取代老的HttpURLConnection
接口;
HttpClient
使用链式调用并通过内置的BodyPublishers
和BodyHandlers
来更方便地处理数据。
RMI远程调用
Java的RMI远程调用是指,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法。RMI是Remote Method Invocation的缩写。
Java提供了RMI实现远程方法调用:
RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务;
RMI的序列化和反序列化可能会造成安全漏洞,因此调用双方必须是内网互相信任的机器,不要把1099端口暴露在公网上作为对外服务。
XML与JSON
XML简介
XML是可扩展标记语言(eXtensible Markup Language)的缩写,它是是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。
DOM
Java提供的DOM API可以将XML解析为DOM结构,以Document对象表示;
DOM可在内存中完整表示XML数据结构;
DOM解析速度慢,内存占用大。
SAX
SAX是Simple API for XML的缩写,它是一种基于流的解析方式,边读取XML边解析,并以事件回调的方式让调用者获取数据。因为是一边读一边解析,所以无论XML有多大,占用的内存都很小。
SAX是一种流式解析XML的API;
SAX通过事件触发,读取速度快,消耗内存少;
调用方必须通过回调方法获得解析过程中的数据。
Jackson
使用Jackson解析XML,可以直接把XML解析为JavaBean,十分方便。
JSON
JSON是JavaScript Object Notation的缩写,它去除了所有JavaScript执行代码,只保留JavaScript的对象格式。
JSON作为数据传输的格式,有几个显著的优点:
- JSON只允许使用UTF-8编码,不存在编码问题;
- JSON只允许使用双引号作为key,特殊字符用
\
转义,格式简单; - 浏览器内置JSON支持,如果把数据用JSON发送给浏览器,可以用JavaScript直接处理。
JSON是轻量级的数据表示方式,常用于Web应用;
Jackson可以实现JavaBean和JSON之间的转换;
可以通过Module扩展Jackson能处理的数据类型;
可以自定义JsonSerializer
和JsonDeserializer
来定制序列化和反序列化。
JDBC编程
Java为关系数据库定义了一套标准的访问接口:JDBC(Java Database Connectivity)
JDBC简介
使用JDBC的好处是:
- 各数据库厂商使用相同的接口,Java代码不需要针对不同数据库分别开发;
- Java程序编译期仅依赖java.sql包,不依赖具体数据库的jar包;
- 可随时替换底层数据库,访问数据库的Java代码基本不变。
JDBC查询
JDBC接口的Connection
代表一个JDBC连接;
使用JDBC查询时,总是使用PreparedStatement
进行查询而不是Statement
;
查询结果总是ResultSet
,即使使用聚合查询也不例外。
JDBC 更新
使用JDBC执行INSERT
、UPDATE
和DELETE
都可视为更新操作;
更新操作使用PreparedStatement
的executeUpdate()
进行,返回受影响的行数。
JDBC事务
数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized
同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
- Atomicity:原子性
- Consistency:一致性
- Isolation:隔离性
- Durability:持久性
JDBC提供了事务的支持,使用Connection可以开启、提交或回滚事务。
JDBC Batch
使用JDBC的batch操作会大大提高执行效率,对内容相同,参数不同的SQL,要优先考虑batch操作。
JDBC 连接池
创建线程是一个昂贵的操作,如果有大量的小任务需要执行,并且频繁地创建和销毁线程,实际上会消耗大量的系统资源,往往创建和消耗线程所耗费的时间比执行任务的时间还长,所以,为了提高效率,可以用线程池。
数据库连接池是一种复用Connection
的组件,它可以避免反复创建新连接,提高JDBC代码的运行效率;
可以配置连接池的详细参数并监控连接池。
Web开发
什么是JavaEE?JavaEE是Java Platform Enterprise Edition的缩写,即Java企业平台。
1 |
|
Web基础
访问网站,使用App时,都是基于Web这种Browser/Server模式,简称BS架构,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web页面,并把Web页面展示给用户即可。
对于Browser来说,请求页面的流程如下:
- 与服务器建立TCP连接;
- 发送HTTP请求;
- 收取HTTP响应,然后把网页在浏览器中显示出来。
使用B/S架构时,总是通过HTTP协议实现通信;
Servlet
Java Servlet 是运行在 Web 服务器的程序,用于处理HTTP请求。
一个Servlet总是继承自HttpServlet
,然后覆写doGet()
或doPost()
方法。
编写Web应用程序就是编写Servlet处理HTTP请求;
Servlet API提供了HttpServletRequest
和HttpServletResponse
两个高级接口来封装HTTP请求和响应;
Web应用程序必须按固定结构组织并打包为.war
文件;
需要启动Web服务器来加载我们的war包来运行Servlet。
Servlet进阶
一个Webapp中的多个Servlet依靠路径映射来处理不同的请求;
映射为/
的Servlet可处理所有“未匹配”的请求;
如何处理请求取决于Servlet覆写的对应方法;
Web服务器通过多线程处理HTTP请求,一个Servlet的处理方法可以由多线程并发执行。
重定向与转发
使用重定向时,浏览器知道重定向规则,并且会自动发起新的HTTP请求;
使用转发时,浏览器并不知道服务器内部的转发逻辑。
Session和Cookie
基于唯一ID识别用户身份的机制称为Session。
…
Spring开发
Spring是一个支持快速开发Java EE应用程序的框架。
IoC容器
容器是一种为某种特定组件的运行提供必要支持的一个软件环境。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!