宝剑锋从撸码出,加油,少年!——克里斯托弗•李
枚举
枚举的定义
枚举类型(enum type
)是指由一组固定的常量组成合法的类型。Java
中由关键字enum
来定义一个枚举类型。下面就是java
枚举类型的定义。
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
Java
定义枚举类型的语句很简约。它有以下特点:
- 使用关键字
enum
- 类型名称,比如这里的
Season
- 一串允许的值,比如上面定义的春夏秋冬四季
- 枚举可以单独定义在一个文件中,也可以嵌在其它
Java
类中
除了这样的基本要求外,用户还有一些其他选择
- 枚举可以实现一个或多个接口(Interface)
- 可以定义新的变量
- 可以定义新的方法
- 可以定义根据具体枚举值而相异的类
定义一个枚举类,该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。
当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
枚举与单例
了解JVM的类加载机制的朋友应该对这部分比较清楚,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。
Enum类
Java中定义枚举是使用enum关键字的,但是Java中其实还有一个java.lang.Enum类。这是一个抽象类,定义如下:
package java.lang;
public abstract class Enum<E extends Enum<E>> implements Constable, Comparable<E>, Serializable {
private final String name;
private final int ordinal;
}
这个类我们在日常开发中不会用到,但是其实我们使用enum定义的枚举,其实现方式就是通过继承Enum类实现的。
当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
byte和Byte
byte是java的基本数据类型,存储整型数据,占据1个字节(8 bits),能够存储的数据范围是-128~+127。
Byte是java.lang中的一个类,目的是为基本数据类型byte进行封装。
Byte是byte的包装类,就如同Integer和int的关系,
一般情况包装类用于泛型或提供静态方法,用于基本类型或字符串之间转换,建议尽量不要用包装类和基本类型之间运算,因为这样运算效率会很差的
字符流、字节流
字节与字符
Bit最小的二进制单位 ,是计算机的操作部分。取值0或者1
Byte(字节)是计算机操作数据的最小单位由8位bit组成 取值(-128-127)
Char(字符)是用户的可读写的最小单位,在Java里面由16位bit组成 取值(0-65535)
字节流
操作byte类型数据,主要操作类是OutputStream、InputStream的子类;不用缓冲区,直接对文件本身操作。
字符流
操作字符类型数据,主要操作类是Reader、Writer的子类;使用缓冲区缓冲字符,不关闭流就不会输出任何内容。
互相转换
整个IO包实际上分为字节流和字符流,但是除了这两个流之外,还存在一组字节流-字符流的转换类。
OutputStreamWriter:是Writer的子类,将输出的字符流变为字节流,即将一个字符流的输出对象变为字节流输出对象。
InputStreamReader:是Reader的子类,将输入的字节流变为字符流,即将一个字节流的输入对象变为字符流的输入对象。
OutputStreamWriter 是字符流通向字节流的桥梁
InputStreamReader 是字节流通向字符流的桥梁
字符流转成字节流
public static void main(String[] args) throws IOException {
File f = new File("test.txt");
// OutputStreamWriter 是字符流通向字节流的桥梁,创建了一个字符流通向字节流的对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(f),"UTF-8");
osw.write("我是字符流转换成字节流输出的");
osw.close();
}
字节流转成字符流
public static void main(String[] args) throws IOException {
File f = new File("test.txt");
InputStreamReader inr = new InputStreamReader(new FileInputStream(f),"UTF-8");
char[] buf = new char[1024];
int len = inr.read(buf);
System.out.println(new String(buf,0,len));
inr.close();
}
同步,异步 和 阻塞,非阻塞之间的区别
同步,异步,是描述被调用方的
如A调用B:
如果是同步,B在接到A的调用后,会立即执行要做的事。A的本次调用可以得到结果。
如果是异步,B在接到A的调用后,不保证会立即执行要做的事,但是保证会去做,B在做好了之后会通知A。A的本次调用得不到结果,但是B执行完之后会通知A。
阻塞、非阻塞,是描述调用方的
同步不一定阻塞,异步也不一定非阻塞。没有必然关系。
举个简单的例子,老张烧水。 1 老张把水壶放到火上,一直在水壶旁等着水开。(同步阻塞) 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 3 老张把响水壶放到火上,一直在水壶旁等着水开。(异步阻塞) 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
1和2的区别是,调用方在得到返回之前所做的事情不一行。 1和3的区别是,被调用方对于烧水的处理不一样。
阻塞式IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
典型的阻塞IO模型的例子为:
data = socket.read();
如果数据没有就绪,就会一直阻塞在read方法。
非阻塞IO模型
当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。
所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
典型的非阻塞IO模型一般如下:
while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}
但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
IO复用模型
多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。
也许有朋友会说,我可以采用 多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
而多路复用IO模式,通过一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用IO比较适合连接数比较多的情况。
另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。
不过要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。
信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
异步IO模型
异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作;而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用iO函数进行实际的读写操作。
注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
BIO、NIO和AIO的区别、三种IO的用法与原理
IO
什么是IO? 它是指计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。
在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。
BIO
Java BIO即Block I/O , 同步并阻塞的IO。
BIO就是传统的java.io包下面的代码实现。
NIO
什么是NIO? NIO 与原来的 I/O 有同样的作用和目的, 他们之间最重要的区别是数据打包和传输的方式。原来的 I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
AIO
Java AIO即Async非阻塞,是异步非阻塞的IO。
区别及联系
BIO (Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。但是实际上线程在等待水壶烧开的时间段什么都没有做。
NIO (New I/O):同时支持阻塞与非阻塞模式,但这里我们以其同步非阻塞I/O模式来说明,那么什么叫做同步非阻塞?如果还拿烧开水来说,NIO的做法是叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。
AIO ( Asynchronous I/O):异步非阻塞I/O模型。异步非阻塞与同步非阻塞的区别在哪里?异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。
各自适用场景
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
netty
Netty是一个非阻塞I/O客户端-服务器框架,主要用于开发Java网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。Netty包括了反应器编程模式的实现。Netty最初由JBoss开发,现在由Netty项目社区开发和维护。
除了作为异步网络应用程序框架,Netty还包括了对HTTP、HTTP2、DNS及其他协议的支持,涵盖了在Servlet容器内运行的能力、对WebSockets的支持、与Google Protocol Buffers的集成、对SSL/TLS的支持以及对用于SPDY协议和消息压缩的支持。自2004年以来,Netty一直在被积极开发
从版本4.0.0开始,Netty在支持NIO和阻塞Java套接字的同时,还支持使用NIO.2作为后端。
本质:JBoss做的一个Jar包
目的:快速开发高性能、高可靠性的网络服务器和客户端程序
优点:提供异步的、事件驱动的网络应用程序框架和工具
序列化与反序列化
序列化是将对象转换为可传输格式的过程。 是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。
反序列化是序列化的逆操作。
序列化是将对象的状态信息转换为可存储或传输的形式的过程。一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。这个相反的过程称为反序列化。
Java对象的序列化与反序列化
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。
但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
相关接口及类
Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
Externalizable接口和Serializable接口的区别
Java中的类通过实现 java.io.Serializable
接口以启⽤其序列化功能。 未实现此接⼜的类将⽆法使其任何状态序列化或反序列化。
可序列化类的所有⼦类型本⾝都是可序列化的。
序列化接⼜没有⽅法或字段, 仅⽤于标识可序列化的语义。
当试图对⼀个对象进⾏序列化的时候, 如果遇到不⽀持Serializable
接口的对象。 在此情况下, 将抛NotSerializableException
。
如果要序列化的类有⽗类, 要想同时将在⽗类中定义过的变量持久化下来, 那么⽗类也应该集成java.io.Serializable
接口。
Externalizable
继承了Serializable
, 该接⼜中定义了两个抽象⽅法:writeExternal()
与readExternal()
。 当使⽤Externalizable
接口来进⾏序列化与反序列化的时候需要开发⼈员重写writeExternal()与readExternal()⽅法。
如果没有在这两个⽅法中定义序列化实现细节, 那么序列化之后, 对象内容为空。
实现Externalizable
接⼜的类必须要提供⼀个public
的⽆参的构造器。
所以, 实现Externalizable
, 并实现writeExternal()
和readExternal()
⽅法可以指定序列化哪些属性。
package com.hollischaung.serialization.ExternalizableDemos;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Created by hollis on 16/2/17.
* 实现Externalizable接口,并实现writeExternal和readExternal方法
*/
public class User2 implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
serialVersionUID
序列化是将对象的状态信息转换为可存储或传输的形式的过程。
我们都知道, Java对象是保存在JVM的堆内存中的, 也就是说, 如果JVM堆不存在了, 那么对象也就跟着消失了。
⽽序列化提供了⼀种⽅案, 可以让你在即使JVM停机的情况下也能把对象保存下来的⽅案。 就像我们平时⽤的U盘⼀样。 把Java对象序列化成可存储或传输的形式( 如⼆进制流) , ⽐如保存在⽂件中。 这样, 当再次需要这个对象的时候, 从⽂件中读取出⼆进制流, 再从⼆进制流中反序列化出对象。
但是, 虚拟机是否允许反序列化, 不仅取决于类路径和功能代码是否⼀致, ⼀个⾮常重要的⼀点是两个类的序列化 ID 是否⼀致, 即serialVersionUID
要求⼀致。
在进⾏反序列化时, JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进⾏⽐较, 如果相同就认为是⼀致的, 可以进⾏反序列化, 否则就会出现序列化版本不⼀致的异常, 即是InvalidCastException
。
这样做是为了保证安全, 因为⽂件存储中的内容可能被篡改。
当实现java.io.Serializable
接口的类没有显式地定义⼀个serialVersionUID
变量时候, Java序列化机制会根据编译的Class⾃动⽣成⼀个serialVersionUID
作序列化版本⽐较⽤, 这种情况下, 如果Class⽂件没有发⽣变化, 就算再编译多 次, serialVersionUID也不会变化的。
但是, 如果发⽣了变化,那么这个⽂件对应的serialVersionUID
也就会发⽣变化。
基于以上原理, 如果我们⼀个类实现了Serializable接口, 但是没有定义serialVersionUID
, 然后序列化。 在序列化之后, 由于某些原因, 我们对该类做了变更, 重新启动应⽤后, 我们相对之前序列化过的对象进⾏反序列化的话就会报错
序列化及反序列化相关知识
1、在Java中,只要一个类实现了java.io.Serializable
接口,那么它就可以被序列化。
2、通过ObjectOutputStream
和ObjectInputStream
对对象进行序列化及反序列化
3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
)
4、序列化并不保存静态变量。
5、要想将父类对象也序列化,就需要让父类也实现Serializable
接口。
6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
ArrayList的序列化
writeObject和readObject方法
在ArrayList中定义了来个方法: writeObject
和readObject
。
这里先给出结论:
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
why transient
ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。
why writeObject and readObject
前面说过,为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient
来声明elementData
。 但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject
和 readObject
方法的方式把其中的元素保留下来。
writeObject
方法把elementData
数组中的元素遍历的保存到输出流(ObjectOutputStream)中。
readObject
方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData
数组中。
至此,我们先试着来回答刚刚提出的问题:
如何自定义的序列化和反序列化策略
答:可以通过在被序列化的类中增加writeObject 和 readObject方法。那么问题又来了:
虽然ArrayList中写了writeObject 和 readObject 方法,但是这两个方法并没有显示的被调用啊。
那么如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的呢?
序列化对单例的破坏
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
为什么序列化可以破坏单例了?
答:序列化会通过反射调用无参数的构造方法创建一个新的对象。
防止序列化对单例的破坏
hasReadResolveMethod
:如果实现了serializable 或者 externalizable接口的类中包含readResolve
则返回true
invokeReadResolve
:通过反射的方式调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
元注解
说简单点,就是 定义其他注解的注解 。 比如Override这个注解,就不是一个元注解。而是通过元注解定义出来的。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这里面的 @Target @Retention 就是元注解。
元注解有四个:@Target(表示该注解可以用于什么地方)、@Retention(表示再什么级别保存该注解信息)、@Documented(将此注解包含再javadoc中)、@Inherited(允许子类继承父类中的注解)。
Java中常用注解使用
@Override 表示当前方法覆盖了父类的方法
@Deprecation 表示方法已经过时,方法上有横线,使用时会有警告。
@SuppressWarnings 表示关闭一些警告信息(通知java编译器忽略特定的编译警告)
SafeVarargs (jdk1.7更新) 表示:专门为抑制“堆污染”警告提供的。
@FunctionalInterface (jdk1.8更新) 表示:用来指定某个接口必须是函数式接口,否则就会编译出错。
Spring常用注解
@Configuration把一个类作为一个IoC容器,它的某个方法头上如果注册了@Bean,就会作为这个Spring容器中的Bean。
@Scope注解 作用域
@Lazy(true) 表示延迟初始化
@Service用于标注业务层组件
@Controller用于标注控制层组件@Repository用于标注数据访问组件,即DAO组件。
@Component泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
@Scope用于指定scope作用域的(用在类上)
@PostConstruct用于指定初始化方法(用在方法上)
@PreDestory用于指定销毁方法(用在方法上)
@DependsOn:定义Bean初始化及销毁时的顺序
@Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
@Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用。如下: @Autowired @Qualifier(“personDaoBean”) 存在多个实例配合使用
@Resource默认按名称装配,当找不到与名称匹配的bean才会按类型装配。
@PostConstruct 初始化注解
@PreDestroy 摧毁注解 默认 单例 启动就加载
如何自定义一个注解
在Java中,类使用class定义,接口使用interface定义,注解和接口的定义差不多,增加了一个@符号,即@interface,代码如下:
public @interface EnableAuth {
}
注解中可以定义成员变量,用于信息的描述,跟接口中方法的定义类似,代码如下:
public @interface EnableAuth {
String name();
}
还可以添加默认值:
public @interface EnableAuth {
String name() default "猿天地";
}
上面的介绍只是完成了自定义注解的第一步,开发中日常使用注解大部分是用在类上,方法上,字段上,示列代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableAuth {
}
Target
用于指定被修饰的注解修饰哪些程序单元,也就是上面说的类,方法,字段
Retention
用于指定被修饰的注解被保留多长时间,分别SOURCE(注解仅存在于源码中,在class字节码文件中不包含),CLASS(默认的保留策略,注解会在class字节码文件中存在,但运行时无法获取),RUNTIME(注解会在class字节码文件中存在,在运行时可以通过反射获取到)三种类型,如果想要在程序运行过程中通过反射来获取注解的信息需要将Retention设置为RUNTIME
Documented
用于指定被修饰的注解类将被javadoc工具提取成文档
Inherited
用于指定被修饰的注解类将具有继承性
什么是泛型
Java泛型( generics) 是JDK 5中引⼊的⼀个新特性, 允许在定义类和接⼜的时候使⽤类型参数( type parameter) 。
声明的类型参数在使⽤时⽤具体的类型来替换。 泛型最主要的应⽤是在JDK 5中的新集合类框架中。
泛型最⼤的好处是可以提⾼代码的复⽤性。 以List接⼜为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。
泛型带来的问题
1、当泛型遇到重载
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List<String>
另一个是List<Integer>
,但是,这段代码是编译通不过的。因为我们前面讲过,参数List<Integer>
和List<String>
编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
2、当泛型遇到catch
如果我们自定义了一个泛型异常类GenericException,那么,不要尝试用多个catch取匹配不同的异常类型,例如你想要分别捕获GenericException、GenericException,这也是有问题的。
3、当泛型内包含静态变量
public class StaticTest{
public static void main(String[] args){
GT<Integer> gti = new GT<Integer>();
gti.var=1;
GT<String> gts = new GT<String>();
gts.var=2;
System.out.println(gti.var);
}
}
class GT<T>{
public static int var=0;
public void nothing(T x){}
}
答案是——2!
由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。
泛型中K T V E ? object等的含义
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类型(无限制通配符类型)
S、U、V - 2nd、3rd、4th types
Object - 是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。
上下界限定符extends 和 super
<? extends T>
和<? super T>
是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
<? extends T>
:是指 “上界通配符(Upper Bounds Wildcards)”,即泛型中的类必须为当前类的子类或当前类。
<? super T>
:是指 “下界通配符(Lower Bounds Wildcards)”,即泛型中的类必须为当前类或者其父类。
在使用泛型时,存取元素时用super,获取元素时,用extends。
频繁往外读取内容的,适合用上界Extends。经常往里插入的,适合用下界Super。
List和原始类型List之间的区别
原始类型List和带参数类型List<Object>
之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。
通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。
它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List<String>
传递给接受 List<Object>
的方法,因为会产生编译错误。
List<?> 和List之间的区别
List<?>
是一个未知类型的List,而List<Object>
其实是任意类型的List。你可以把List<String>
, List<Integer>
赋值给List<?>
,却不能把List<String>
赋值给 List<Object>
。
内存数据库(h2)
H2是一个开源的嵌入式(非嵌入式设备)数据库引擎,它是一个用Java开发的类库,可直接嵌入到应用程序中,与应用程序一起打包发布,不受平台限制。
H2与Derby、HSQLDB、MySQL、PostgreSQL等开源数据库相比,H2的优势为:
- Java开发,不受平台限制;
- H2只有一个jar包,占用空间小,适合嵌入式数据库;
- 有web控制台,用于管管理数据库。
Error和Exception
Exception和 Error, ⼆者都是 Java异常处理的重要⼦类, 各⾃都包含⼤量⼦类。均继承自Throwable类。
Error表⽰系统级的错误, 是java运⾏环境内部错误或者硬件问题, 不能指望程序来处理这样的问题, 除了退出运⾏外别⽆选择, 它是Java虚拟机抛出的。
Exception 表⽰程序需要捕捉、 需要处理的常, 是由与程序设计的不完善⽽出现的问题, 程序必须处理的问题。
异常类型
Java中的异常, 主要可以分为两⼤类, 即受检异常( checked exception) 和 ⾮受检异常( unchecked exception)
受检异常
对于受检异常来说, 如果⼀个⽅法在声明的过程中证明了其要有受检异常抛出:
public void test() throw new Exception{ }
那么,当我们在程序中调⽤他的时候, ⼀定要对该异常进⾏处理( 捕获或者向上抛出) , 否则是⽆法编译通过的。 这是⼀种强制规范。
这种异常在IO操作中⽐较多。 ⽐如FileNotFoundException , 当我们使⽤IO流处理⼀个⽂件的时候, 有⼀种特殊情况, 就是⽂件不存在, 所以, 在⽂件处理的接⼜定义时他会显⽰抛出FileNotFoundException, ⽬的就是告诉这个⽅法的调⽤者,我这个⽅法不保证⼀定可以成功, 是有可能找不到对应的⽂件 的, 你要明确的对这种情况做特殊处理哦。
所以说, 当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。
非受检异常
对于⾮受检异常来说, ⼀般是运⾏时异常, 继承⾃RuntimeException。 在编写代码的时候, 不需要显⽰的捕获,但是如果不捕获, 在运⾏期如果发⽣异常就会中断程序的执⾏。
这种异常⼀般可以理解为是代码原因导致的。 ⽐如发⽣空指针、 数组越界等。 所以, 只要代码写的没问题, 这些异常都是可以避免的。 也就不需要我们显⽰的进⾏处理。
试想⼀下, 如果你要对所有可能发⽣空指针的地⽅做异常处理的话, 那相当于你的所有代码都需要做这件事。
异常相关关键字
throws、 throw、 try、 catch、 finally
try⽤来指定⼀块预防所有异常的程序;
catch⼦句紧跟在try块后⾯, ⽤来指定你想要捕获的异常的类型;
finally为确保⼀段代码不管发⽣什么异常状况都要被执⾏;
throw语句⽤来明确地抛出⼀个异常;
throws⽤来声明⼀个⽅法可能抛出的各种异常;
try-with-resources
Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:
public static void main(String[] args) {
BufferedReader br = null;
try {
String line;
br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
// handle exception
}
}
}
从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:
public static void main(String... args) {
try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
}
}
finally和return的执行顺序
try()
⾥⾯有⼀个return
语句, 那么后⾯的finally{}
⾥⾯的code会不会被执⾏, 什么时候执⾏, 是在return
前还是return
后?
如果try中有return语句, 那么finally中的代码还是会执⾏。因为return表⽰的是要整个⽅法体返回, 所以,finally中的语句会在return之前执⾏。
但是return前执行的finally块内,对数据的修改效果对于引用类型和值类型会不同
// 测试 修改值类型
static int f() {
int ret = 0;
try {
return ret; // 返回 0,finally内的修改效果不起作用
} finally {
ret++;
System.out.println("finally执行");
}
}
// 测试 修改引用类型
static int[] f2(){
int[] ret = new int[]{0};
try {
return ret; // 返回 [1],finally内的修改效果起了作用
} finally {
ret[0]++;
System.out.println("finally执行");
}
}
SimpleDateFormat的线程安全性
SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。这其实就是问题的关键。
由于我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。
假设线程1刚刚执行完calendar.setTime
把时间设置成2018-11-11,还没等执行完,线程2又执行了calendar.setTime
把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的calendar.getTime
得到的时间就是线程2改过之后的。
除了format方法以外,SimpleDateFormat的parse方法也有同样的问题。
所以,不要把SimpleDateFormat作为一个共享变量使用。
解决方法:主要的几个手段有改为局部变量、使用synchronized加锁、使用Threadlocal为每一个线程单独创建一个等。
Java 8中的时间处理
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
- 非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
- 设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
- 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
在Java8中, 新的时间及⽇期API位于java.time包中, 该包中有哪些重要的类。 分别代表了什么?
Instant
: 时间戳
Duration
: 持续时间, 时间差
LocalDate
: 只包含⽇期, ⽐如: 2016-10-20
LocalTime
: 只包含时间, ⽐如: 23[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W4tAWwlk-1605697791091)(https://github.githubassets.com/images/icons/emoji/12.png)]10
LocalDateTime
: 包含⽇期和时间, ⽐如: 2016-10-20 23[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEC4SCLv-1605697791095)(https://github.githubassets.com/images/icons/emoji/14.png)]21
Period
: 时间段
ZoneOffset
: 时区偏移量, ⽐如: +8:00
ZonedDateTime
: 带时区的时间
Clock
: 时钟, ⽐如获取⽬前美国纽约的时间
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
LocalTime 和 LocalDate
LocalDate
表⽰⽇期, 年⽉⽇, LocalTime
表⽰时间, 时分 秒
获取当前时间
在Java8中,使用如下方式获取当前时间:
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
System.out.printf("Year : %d Month : %d day : %d t %n", year,month, day);
创建指定日期的时间
LocalDate date = LocalDate.of(2018, 01, 01);
检查闰年
直接使⽤LocalDate的isLeapYear即可判断是否闰年
LocalDate nowDate = LocalDate.now();
//判断闰年
boolean leapYear = nowDate.isLeapYear();
计算两个⽇期之间的天数和⽉数
在Java 8中可以⽤java.time.Period类来做计算。
Period period = Period.between(LocalDate.of(2018, 1, 5),LocalDate.of(2018, 2, 5));