Java 基础
OOP相关
21、讲讲类的实例化顺序?
答:
-
执行clinit方法
什么是clinit得等到jvm篇讲解的时候讲方便点,这里就记住clinit是class的static 变量,final变量,static代码块这些就好。clinit方法不是每个class都有的,Object就没有。
包括两个步骤:
1)执行父类的clinit方法。
2)执行子类的clinit方法。
clinit里面具体的执行逻辑是:
① 执行static filed
②执行static 代码块
-
执行init方法(构造函数)
1)执行父类的init方法。什么是init也是得等到jvm篇讲解的时候说比较方便,这里记住init方法就是我们class的构造函数就好。
2)执行子类的init方法。
执行完构造函数之后,jvm会把赋值的对象和构造的对象关联起来,这样就完成了父类的实例化。
22、char能否存一个中文字符
答:
能,C++中char是1byte,但是在Java中Char采用ACSII编码,char是2bytes,所以Java中char能保存两位字符。
23、error 和 exception 有什么区别?
答:
error是jvm的底层自己内部的异常。
exception是jdk之上的异常,分受检异常和非受检异常。受检异常是调用者需要try……catch的异常,非受检异常是用户不需要try……catch的异常。RuntimeException都是非受检异常。
受检异常:CheckedException,非受检异常:RuntimeException。
24、反射的用途
答:
在日常使用里面我们会有一种需求:需要在运行的时候知道某个对象里面有什么字段,需要在执行的某方法方法的前后执行一下一些统一逻辑。
JDK在解析字节码(class)的时候,保留了字节码的描述,保存在内存的方法区。JDK为了满足我们上面类似需求提供给了我们一些方式,让我们能获取字节码的描述,然后根据自己的需求来实现自己想要的功能。
25、反射的三种方式
答:
-
类.class。
比如
Class<?> clazz = A.class
-
对象.getClass()
比如:
Class<?> clazz = a.getClass()
-
Class.forName()
比如:
Class<?> clazz = Class.forName("com.jdbc.xxxx")
26、什么时候用断言
答:
断言其实是我们告诉编译器,我们业务逻辑上保证这个东西是正确的。比如保证某个东西一定不会为null
assert obj != null;
在我们的平时开发/测试的时候,我们在运行jvm的时候加上参数 -ea
,断言就生效了,会帮我们做一些判断,执行的逻辑不符合断言就会抛出一些异常。生产环境需要去掉。
我也自己封装了一套断言程序:
/**
* 这个类是用来判断对象是否为空的。断言工具 <br/>
*
* 时间 2020-08-06
* @author 袁小黑
*/
public enum AssertUtils {
;
/**
* 使用枚举类的原因是因为这个类是一个工具类,防止被别人初始化
*/
public static void isNotNull(Object obj, String msg, Object...pars) {
if (obj == null) {
throw new RuntimeException(MessageFormatter.arrayFormat(msg, pars).getMessage());
}
}
public static void isTrue(Boolean isTrue, String msg, Object...pars) {
if (isTrue == null || !isTrue) {
throw new RuntimeException(MessageFormatter.arrayFormat(msg, pars).getMessage());
}
}
public static void isNotBlank(String str, String msg, Object...pars) {
if (StringUtils.isBlank(str)) {
throw new RuntimeException(MessageFormatter.arrayFormat(msg, pars).getMessage());
}
}
public static void isNotEmpty(Collection<?> collection, String msg, Object...pars) {
if (collection == null || collection.isEmpty()) {
throw new RuntimeException(MessageFormatter.arrayFormat(msg, pars).getMessage());
}
}
public static void isNatureNumber(int num, String msg, Object...pars) {
if (num < 0) {
throw new RuntimeException(MessageFormatter.arrayFormat(msg, pars).getMessage());
}
}
}
17、Java IO 的理解
答:
Java IO,什么是IO。IO:input、output。输入输出,其实描述的是数据从一个地方到另外一个地方的逻辑,Java IO 它站的是内存的角度。比如从内存到磁盘,从内存到网络,这个就是java io。
从技术角度上讲Java IO主要分类字节流、字符流。为什么要分字符流和字节流呢?
字符:在java中的字符就是char,采用的是ASCII编码处理Unicode字符,**Java为了让我们更加方便地操纵Unicode字符,故意加多了一种IO方式。**之前谈过Java中一个char可以存一个中文字符,那么使用字符IO就不会出现传输过程中出现异常情况的时候导致某个字只传了一半(当然,这里不考虑网络IO的问题。)字符编码都是从Reader和Writer抽象类中基础出来的。只能操纵文本类型的文件,如果使用Reader和Writer操作音频/视频文件就会出现文件损坏的情况。
字节:最基本的流,适用于所有的情况。字节输入流输出流,其实没有缓存,直接使用了磁盘IO,一般都会很慢的,字符流还是用了缓存,性能比字节流强很多。实际操作文件直接使用字节流的比较少。
使用了缓存和不使用缓存的差别还是很大的,下面是我的测试用例:
//write elapse time : 3127
//read elapse time : 24525
@Test
public void test1() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
try (FileInputStream fis = new FileInputStream(rootPath+ File.separator+"test.txt");
FileOutputStream fos = new FileOutputStream(rootPath+ File.separator+"test.txt", true)
) {
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
fos.write("xxxxxxxxxxx\n".getBytes());
}
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
int b;
statBegin = System.currentTimeMillis();
while ((b = fis.read()) != -1) {
//System.out.print((char)b);
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//write elapse time : 78
//read elapse time : 479
@Test
public void test() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
try (OutputStreamWriter fw = new FileWriter(rootPath+File.pathSeparator+"test.txt");
InputStreamReader fr = new FileReader(rootPath+File.pathSeparator+"test.txt")
) {
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
fw.write("xxxxxxxxxxx\n");
}
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
int b;
statBegin = System.currentTimeMillis();
while ((b = fr.read()) != -1) {
//System.out.print((char)b);
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
} catch (IOException e) {
e.printStackTrace();
}
}
很简单的一段逻辑,对test.txt
文件写1百万次和读1百万次,写的场景:字符流花了78ms,字节流花了3127ms,相差了40+倍的时间,读的场景:字符流479ms,字节流花了24525ms,相差也是接近50倍,这就是缓存的魅力。参考apache的commons-io,建议使用字符流的时候自己加上一个buffer。
字符流字节流之下都可以再分输入流/输出流。流又设计序列化的问题,IO这一块的复杂接触之后就能知道,还是那句话:没有最完美的方案,只有最适合的方案。很多种IO提出来之后是为了解决某个问题,我们要具体看这个解决方案适不适合。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5AdcG2BE-1604224920316)(java%E5%9F%BA%E7%A1%803.assets/1018541-20170317090935213-142491173.png)]
如果平时需要性能比较高的场景,建议使用:缓冲流,有:BufferedInputStream、BufferOutputStream 、BufferedReader、BufferWriter。
同样是读写1百万次,下面是针对Buffer类型的流进行的一组实验:
//write elapse time : 57
//read elapse time : 58
@Test
public void test1() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
try (FileInputStream fis = new FileInputStream(rootPath+ File.separator+"test.txt");
FileOutputStream fos = new FileOutputStream(rootPath+ File.separator+"test.txt", true);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
) {
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
bos.write("xxxxxxxxxxx\n".getBytes());
}
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
int b;
statBegin = System.currentTimeMillis();
while ((b = bis.read()) != -1) {
//System.out.print((char)b);
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
//write elapse time : 45
//read elapse time : 53
@Test
public void test() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
try (OutputStreamWriter fw = new FileWriter(rootPath+File.pathSeparator+"test.txt");
InputStreamReader fr = new FileReader(rootPath+File.pathSeparator+"test.txt");
BufferedReader br = new BufferedReader(fr);
BufferedWriter bw = new BufferedWriter(fw);
) {
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
bw.write("xxxxxxxxxxx\n");
}
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
int b;
statBegin = System.currentTimeMillis();
while ((b = br.read()) != -1) {
//System.out.print((char)b);
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
} catch (IOException e) {
e.printStackTrace();
}
}
从上面的代码可以看出:写的场景:对于字节流来说使用缓存几乎提升了40+倍的性能(elapse time 表示运行这段程序花费的时间),对于字符流来说,其内核已经实现了缓存,使用缓存流提升不是很大,但也是接近了一倍。读的场景:性能都有很大提升。其实使用缓存可靠性就会低点,所有的东西都取决于业务需求。
其实上面的样例都是顺序读写。
18、Java NIO的理解
答:
NIO:new io。是jdk4之后引入的新类型的IO,主要解决访问io资源的过程中的性能问题。
Nio的三个核心部件有:Selector
、Channel
、Buffer
。
Channel
有FileChannel,NioChannel,DatagramChannel等,不同的需求使用不同的Channel。
Buffer
就更加多了,可以说是多到恐怖的境界,几乎一种数据类型一种buffer,最重要的是ByteBuffer
。
Selector
只有一种,这点可以放心。
FileChannel
是在文件方面的nio,其它的Channel都是网络协议的Channel。FileChannel的应用非常广泛,RocketMq的commitLog就是使用FileChannel进行mmap文件到磁盘的映射。
这里针对上面的17问,来做一个实验,也是针对一个文件写读1百万次:
//write elapse time : 3023
//read elapse time : 3
@Test
public void writeAndRead() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
String targetFile = rootPath+ File.separator+"test.txt";
// 分配1MB的缓存空间
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
try (FileInputStream fis = new FileInputStream(rootPath+ File.separator+"test.txt");
FileOutputStream fos = new FileOutputStream(rootPath+ File.separator+"test.txt")
) {
FileChannel channel = fos.getChannel();
FileChannel rcfChannel = fis.getChannel();
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
byteBuffer.put("xxxxxxxxxxx\n".getBytes());
byteBuffer.flip();
channel.write(byteBuffer);
byteBuffer.clear();
}
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
int b;
byteBuffer.flip();
statBegin = System.currentTimeMillis();
while (rcfChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
//System.out.println(getString(byteBuffer));
byteBuffer.clear();
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
最后结果是:写的时间变成了3s,但是读的时间直接变成了3ms。其实17问给了例子都是顺序读写,顺序读写的性能是比随机读写快很多的,ES也是依据顺序读写去实现的。使用FileChannel读的性能提高是非常明显的。
FileChannel还有很多妙用,我们来试试传说中的零拷贝。
mmap()
//write elapse time : 74
//read elapse time : 12
@Test
public void mmap() {
String rootPath = Thread.currentThread().getContextClassLoader().getResource("").toString().substring(6);
String targetFile = rootPath+ File.separator+"test.txt";
try (FileChannel channel = FileChannel.open(Path.of(targetFile), EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.READ));
FileChannel rcfChannel = FileChannel.open(Path.of(targetFile), EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.READ));
) {
int fileSize = 30 * 1024 * 1024;
MappedByteBuffer writeMappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);
long statBegin = System.currentTimeMillis();
for (int i = 0; i < 100_0000; i++) {
writeMappedByteBuffer.put("xxxxxxxxxxx\n".getBytes());
}
writeMappedByteBuffer.force();
System.out.println("write elapse time : "+(System.currentTimeMillis() - statBegin));
MappedByteBuffer readMappedByteBuffer = rcfChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
statBegin = System.currentTimeMillis();
//byte[] ds = new byte[fileSize];
for (int offset = 0; offset < fileSize; offset++) {
ByteBuffer b = readMappedByteBuffer.slice(0, fileSize);
//System.out.println(getString(b));
}
System.out.println("read elapse time : "+(System.currentTimeMillis() - statBegin));
//System.out.println(new String(ds));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
从上面可以看出零拷贝的mmap方式花费的时间是写场景花的是74ms,读场景花的时间是12ms。在读的时候,它具有很大的优势,因为它可以随意读,只需要知道你需要的数据在buffer的哪个偏移量那里,它的缺点是:需要消耗大量的内存,这也是空间换时间的精髓。
和我们在17问做的东西有很大的不同之处,因为我们在17问做的实验都是顺序读写,在随机读写中我们一般使用RandomAccessFile
来实现,RandomAccessFile
配合FileChannel实现零拷贝,技术更加完美;缺点也是需要大量的内存。
而且现实业务很多都是随机读写的,RandomAccessFile在针对大文件处理的时候非常有优势。RocketMQ的消息采用顺序写到commitlog文件,然后利用consume queue文件作为索引;RocketMQ采用零拷贝mmap+write的方式来回应Consumer的请求;和我们上面的实验代码非常相似。
零拷贝还有一种方式就是:transferTo和transferFrom。Kafka使用的就是transfer和transferFrom,这两种拷贝方式在性能上比mmap的零拷贝方式还快。但是tranferTo和transferFrom 应对的需求是:经常需要从一个位置将文件传输到另外一个位置。FileChannel提供了transferTo()方法用来提高传输的效率。
ile` 配合FileChannel实现零拷贝,技术更加完美;缺点也是需要大量的内存。
而且现实业务很多都是随机读写的,RandomAccessFile在针对大文件处理的时候非常有优势。RocketMQ的消息采用顺序写到commitlog文件,然后利用consume queue文件作为索引;RocketMQ采用零拷贝mmap+write的方式来回应Consumer的请求;和我们上面的实验代码非常相似。
零拷贝还有一种方式就是:transferTo和transferFrom。Kafka使用的就是transfer和transferFrom,这两种拷贝方式在性能上比mmap的零拷贝方式还快。但是tranferTo和transferFrom 应对的需求是:经常需要从一个位置将文件传输到另外一个位置。FileChannel提供了transferTo()方法用来提高传输的效率。