Java笔记
Java快速入门
1.1第一个java程序
1 | public class test1{ |
继承
Student是Person子类
向上转型
1 | Student s = new Student(); |
向下转型
1 | Person p1 = new Student(); // upcasting, ok |
instanceof,在向下转移前可以先判断
多态
Override是覆写(重写)Overload是新方法
接口
定义一个接口用interface
实现一个接口用implements
java核心类
1.1 字符串和编码
比较字符串用equals(),要忽略大小写比较,使用equalsIgnoreCase()方法
学习地址:https://www.liaoxuefeng.com/wiki/1252599548343744/1260469698963456
1.2 StringBuilder
StringBuffer,这是Java早期的一个StringBuilder的线程安全版本,它通过同步来保证多个线程操作StringBuffer也是安全的,但是同步会带来执行速度的下降。
StringBuilder和StringBuffer接口完全相同,现在完全没有必要使用StringBuffer。
1.3 包装类型
基本类型:
byte,short,int,long,boolean,float,double,char引用类型:所有
class和interface类型基本类型 对应的引用类型 boolean java.lang.Boolean byte java.lang.Byte short java.lang.Short int java.lang.Integer long java.lang.Long float java.lang.Float double java.lang.Double char java.lang.Character
1.X BigInteger
java.math.BigInteger用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数;
和long型整数运算比,BigInteger不会有范围限制,但缺点是速度比较慢;
BigInteger和Integer、Long一样,也是不可变类,并且也继承自Number类;
使用longValueExact()方法时,如果超出了long型的范围,会抛出ArithmeticException;
如果BigInteger的值超过了float的最大范围,返回 Infinity 。
1.X BigDecimal
BigDecimal用于表示精确的小数,常用于财务计算;
比较BigDecimal的值是否相等,必须使用compareTo()而不能使用equals()。
1.x Java提供的常用工具类有:
- Math:数学计算
- Random:生成伪随机数
- SecureRandom:生成安全的随机数
异常处理
因为Java的异常是class,它的继承关系如下:
1 | ┌───────────┐ |
异常类型
1 | Exception |
日志,使用JDK Logging
JDK的Logging定义了7个日志级别,从严重到普通:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
因为默认级别是INFO,因此,INFO级别以下的日志,不会被打印出来。使用日志级别的好处在于,调整级别,就可以屏蔽掉很多调试相关的日志输出。
日志,使用Commons Logging
默认情况下,Commons Loggin自动搜索并使用Log4j(Log4j是另一个流行的日志系统),如果没有找到Log4j,再使用JDK Logging。
Commons Logging定义了6个日志级别:
- FATAL
- ERROR
- WARNING
- INFO
- DEBUG
- TRACE
默认级别是INFO。
反射
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实例比较和instanceof的差别:
1 | Integer n = new Integer(123); |
用instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个class的时候,我们才使用==判断class实例。
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:
1 | // 获取String的Class实例: |
上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
访问字段
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
- getDeclaredFiled 仅能获取类本身的属性成员(包括私有、共有、保护)
- getField 仅能获取类(及其父类可以自己测试) public属性成员
Java的反射API提供的Field类封装了字段的所有信息:
通过Class实例的方法可以获取Field实例:getField(),getFields(),getDeclaredField(),getDeclaredFields();
通过Field实例可以获取字段信息:getName(),getType(),getModifiers();
通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用setAccessible(true)来访问非public字段。
通过反射读写字段是一种非常规方法,它会破坏对象的封装。
调用方法
Method getMethod(name, Class...):获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)Method[] getMethods():获取所有public的Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
一个Method对象包含一个方法的所有信息:
getName():返回方法名称,例如:"getScore";getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
小结
Java的反射API提供的Method对象封装了方法的所有信息:
通过Class实例的方法可以获取Method实例:getMethod(),getMethods(),getDeclaredMethod(),getDeclaredMethods();
通过Method实例可以获取方法信息:getName(),getReturnType(),getParameterTypes(),getModifiers();
通过Method实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters);
通过设置setAccessible(true)来访问非public方法;
通过反射调用方法时,仍然遵循多态原则。
调用构造方法
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
getConstructor(Class...):获取某个public的Constructor;getDeclaredConstructor(Class...):获取某个Constructor;getConstructors():获取所有public的Constructor;getDeclaredConstructors():获取所有Constructor。
Constructor对象封装了构造方法的所有信息;
通过Class实例的方法可以获取Constructor实例:getConstructor(),getConstructors(),getDeclaredConstructor(),getDeclaredConstructors();
通过Constructor实例可以创建一个实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。
获取继承关系
小结
通过Class对象可以获取继承关系:
Class getSuperclass():获取父类类型;Class[] getInterfaces():获取当前类实现的所有接口。
通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
泛型
Java的泛型是采用擦拭法实现的;
擦拭法决定了泛型<T>:
- 不能是基本类型,例如:
int; - 不能获取带泛型类型的
Class,例如:Pair<String>.class; - 不能判断带泛型类型的类型,例如:
x instanceof Pair<String>; - 不能实例化
T类型,例如:new T()。
泛型方法要防止重复定义方法,例如:public boolean equals(T obj);
子类可以获取父类的泛型类型<T>。
小结
使用类似<? extends Number>通配符作为方法参数时表示:
- 方法内部可以调用获取
Number引用的方法,例如:Number n = obj.getFirst();; - 方法内部无法调用传入
Number引用的方法(null除外),例如:obj.setFirst(Number n);。
即一句话总结:使用extends通配符表示可以读,不能写。
使用类似<T extends Number>定义泛型类时表示:
- 泛型类型限定为
Number以及Number的子类。
对比extends和super通配符
我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
无限定通配符
因为<?>通配符既没有extends,也没有super,因此:
- 不允许调用
set(T)方法并传入引用(null除外); - 不允许调用
T get()方法并获取T引用(只能获取Object引用)。
换句话说,既不能读,也不能写,那只能做一些null判断:
小结
使用类似<? super Integer>通配符作为方法参数时表示:
- 方法内部可以调用传入
Integer引用的方法,例如:obj.setFirst(Integer n);; - 方法内部无法调用获取
Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。
即使用super通配符表示只能写不能读。
使用extends和super通配符要遵循PECS原则。
无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。
集合
HashMap
要正确使用HashMap,作为key的类必须正确覆写equals()和hashCode()方法;
一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是:
- 如果
equals()返回true,则hashCode()返回值必须相等; - 如果
equals()返回false,则hashCode()返回值尽量不要相等。
实现hashCode()方法可以通过Objects.hashCode()辅助方法实现。
TreeMap
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用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set<E>:boolean add(E e) - 将元素从
Set<E>删除:boolean remove(Object e) - 判断是否包含元素:
boolean contains(Object e)
Queue
在Java的标准库中,队列接口Queue定义了以下几个方法:
int size():获取队列长度;boolean add(E)/boolean offer(E):添加元素到队尾;E remove()/E poll():获取队首元素并从队列中删除;E element()/E peek():获取队首元素但并不从队列中删除。
PriorityQueue
PriorityQueue实现了一个优先队列:从队首获取元素时,总是获取优先级最高的元素。
PriorityQueue默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)。
Deque
| Queue | Deque | |
|---|---|---|
| 添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
| 取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
| 取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
| 添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
| 取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
| 取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
Deque实现了一个双端队列(Double Ended Queue),它可以:
- 将元素添加到队尾或队首:
addLast()/offerLast()/addFirst()/offerFirst(); - 从队首/队尾获取元素并删除:
removeFirst()/pollFirst()/removeLast()/pollLast(); - 从队首/队尾获取元素但不删除:
getFirst()/peekFirst()/getLast()/peekLast(); - 总是调用
xxxFirst()/xxxLast()以便与Queue的方法区分开; - 避免把
null添加到队列。
Stack
IO流
IO流是一种流式的数据输入/输出模型:
- 二进制数据以
byte为最小单位在InputStream/OutputStream中单向流动; - 字符数据以
char为最小单位在Reader/Writer中单向流动。
Java标准库的java.io包提供了同步IO功能:
- 字节流接口:
InputStream/OutputStream; - 字符流接口:
Reader/Writer。
File
Java标准库的java.io.File对象表示一个文件或者目录:
- 创建
File对象本身不涉及IO操作; - 可以获取路径/绝对路径/规范路径:
getPath()/getAbsolutePath()/getCanonicalPath(); - 可以获取目录的文件和子目录:
list()/listFiles(); - 可以创建或删除文件和目录。
InputStream
Java标准库的java.io.InputStream定义了所有输入流的超类:
FileInputStream实现了文件流输入;ByteArrayInputStream在内存中模拟一个字节流输入。
总是使用try(resource)来保证InputStream正确关闭。
OutputStream
Java标准库的java.io.OutputStream定义了所有输出流的超类:
FileOutputStream实现了文件流输出;ByteArrayOutputStream在内存中模拟一个字节流输出。
某些情况下需要手动调用OutputStream的flush()方法来强制输出缓冲区。
总是使用try(resource)来保证OutputStream正确关闭。
1 | input.transferTo(output); // copy功能;目录已存在 |
Reader
Reader定义了所有字符输入流的超类:
FileReader实现了文件字符流输入,使用时需要指定编码;CharArrayReader和StringReader可以在内存中模拟一个字符流输入。
Reader是基于InputStream构造的:可以通过InputStreamReader在指定编码的同时将任何InputStream转换为Reader。
总是使用try (resource)保证Reader正确关闭。
使用InputStreamReader,可以把一个InputStream转换成一个Reader。
Writer
Writer定义了所有字符输出流的超类:
FileWriter实现了文件字符流输出;CharArrayWriter和StringWriter在内存中模拟一个字符流输出。
使用try (resource)保证Writer正确关闭。
Writer是基于OutputStream构造的,可以通过OutputStreamWriter将OutputStream转换为Writer,转换时需要指定编码。
正则表达式
小结
单个字符的匹配规则如下:
| 正则表达式 | 规则 | 可以匹配 |
|---|---|---|
A |
指定字符 | A |
\u548c |
指定Unicode字符 | 和 |
. |
任意字符 | a,b,&,0 |
\d |
数字0~9 | 0~`9` |
\w |
大小写字母,数字和下划线 | az,AZ,0~`9,_` |
\s |
空格、Tab键 | 空格,Tab |
\D |
非数字 | a,A,&,_,…… |
\W |
非\w | &,@,中,…… |
\S |
非\s | a,A,&,_,…… |
多个字符的匹配规则如下:
| 正则表达式 | 规则 | 可以匹配 |
|---|---|---|
A* |
任意个数字符 | 空,A,AA,AAA,…… |
A+ |
至少1个字符 | A,AA,AAA,…… |
A? |
0个或1个字符 | 空,A |
A{3} |
指定个数字符 | AAA |
A{2,3} |
指定范围个数字符 | AA,AAA |
A{2,} |
至少n个字符 | AA,AAA,AAAA,…… |
A{0,3} |
最多n个字符 | 空,A,AA,AAA |
小结
复杂匹配规则主要有:
| 正则表达式 | 规则 | 可以匹配 |
|---|---|---|
| ^ | 开头 | 字符串开头 |
| $ | 结尾 | 字符串结束 |
| [ABC] | […]内任意字符 | A,B,C |
| [A-F0-9xy] | 指定范围的字符 | A,……,F,0,……,9,x,y |
| [^A-F] | 指定范围外的任意字符 | 非A~`F` |
| AB|CD|EF | AB或CD或EF | AB,CD,EF |
多线程
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;
一个线程对象只能调用一次start()方法;
线程的执行代码写在run()方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
Thread.sleep()可以把当前线程暂停一段时间。
线程的优先级
可以对线程设定优先级,设定优先级的方法是:
1 | Thread.setPriority(int n) // 1~10, 默认值5 |
优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
中断线程
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
通过标志位判断需要正确使用volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。
线程同步
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
同步方法
用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;
多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。
使用wait和notify
wait和notify用于多线程协调运行:
- 在
synchronized内部可以调用wait()使线程进入等待状态; - 必须在已获得的锁对象上调用
wait()方法; - 在
synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程; - 必须在已获得的锁对象上调用
notify()或notifyAll()方法; - 已唤醒的线程还需要重新获得锁后才能继续执行。
使用ReentrantLock
ReentrantLock可以替代synchronized进行同步;
ReentrantLock获取锁更安全;
必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁;
可以使用tryLock()尝试获取锁。
使用Condition
可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。
Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的:
await()会释放当前锁,进入等待状态;signal()会唤醒某个等待线程;signalAll()会唤醒所有等待线程;- 唤醒线程从
await()返回后需要重新获得锁。
此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:
1 | if (condition.await(1, TimeUnit.SECOND)) { |
可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。
ReadWriteLock
使用ReadWriteLock可以提高读取效率:
ReadWriteLock只允许一个线程写入;ReadWriteLock允许多个线程在没有写入时同时读取;ReadWriteLock适合读多写少的场景。
使用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 |
ExecutorService
JDK提供了ExecutorService实现了线程池功能:
线程池内部维护一组线程,可以高效执行大量小任务;
Executors提供了静态方法创建不同类型的ExecutorService;必须调用
shutdown()关闭ExecutorService;ScheduledThreadPool可以定期调度多个任务。思考:
Q1:在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?
A1:If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.
译:如果此任务的任何执行时间超过其周期,则后续执行可能会延迟开始,但不会并发执行。
Q2:如果任务抛出了异常,后续任务是否继续执行?
A2:If any execution of the task encounters an exception, subsequent executions are suppressed.
译:如果任务的任何执行遇到异常,则将禁止后续任务的执行。
使用Future
当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。
一个Future<V>接口表示一个未来可能会返回的结果,它定义的方法有:
get():获取结果(可能会等待)get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;cancel(boolean mayInterruptIfRunning):取消当前任务;isDone():判断任务是否已完成。
网络编程
TCP编程
使用Java进行TCP编程时,需要使用Socket模型:
- 服务器端用
ServerSocket监听指定端口; - 客户端使用
Socket(InetAddress, port)连接服务器; - 服务器端用
accept()接收连接并返回Socket; - 双方通过
Socket打开InputStream/OutputStream读写数据; - 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
flush()用于强制输出缓冲区到网络。
UDP
使用UDP协议通信时,服务器和客户端双方无需建立连接:
- 服务器端用
DatagramSocket(port)监听端口; - 客户端使用
DatagramSocket.connect()指定远程地址和端口; - 双方通过
receive()和send()读写数据; DatagramSocket没有IO流接口,数据被直接写入byte[]缓冲区。



