熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。
- [BIO,NIO,AIO 总结](#bionioaio-总结)
- [1. BIO \(Blocking I/O\)](#1-bio-blocking-io)
- [1.1 传统 BIO](#11-传统-bio)
- [1.2 伪异步 IO](#12-伪异步-io)
- [1.3 代码示例](#13-代码示例)
- [1.4 总结](#14-总结)
- [2. NIO \(New I/O\)](#2-nio-new-io)
- [2.1 NIO 简介](#21-nio-简介)
- [2.2 NIO的特性/NIO与IO区别](#22-nio的特性nio与io区别)
- [1)Non-blocking IO(非阻塞IO)](#1non-blocking-io(非阻塞io))
- [2)Buffer\(缓冲区\)](#2buffer缓冲区)
- [3)Channel \(通道\)](#3channel-通道)
- [4)Selectors\(选择器\)](#4selectors选择器)
- [2.3 NIO 读数据和写数据方式](#23-nio-读数据和写数据方式)
- [2.4 NIO核心组件简单介绍](#24-nio核心组件简单介绍)
- [2.5 代码示例](#25-代码示例)
- [3. AIO \(Asynchronous I/O\)](#3-aio-asynchronous-io)
- [参考](#参考)
# BIO,NIO,AIO 总结
Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。
在讲 BIO,NIO,AIO 之前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。
**同步与异步**
- **同步:** 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- **异步:** 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
**阻塞和非阻塞**
- **阻塞:** 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- **非阻塞:** 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(**同步阻塞**)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(**同步非阻塞**)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(**异步非阻塞**)。
## 1. BIO (Blocking I/O)
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
### 1.1 传统 BIO
BIO通信(一请求一应答)模型图如下(图源网络,原出处不明):
![传统BIO通信模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2.png)
采用 **BIO 通信模型** 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在`while(true)` 循环中服务端会调用 `accept()` 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。
如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 **一请求一应答通信模型** 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。使用`FixedThreadPool` 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。
**我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?**
在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。
### 1.2 伪异步 IO
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
伪异步IO模型图(图源网络,原出处不明):
![伪异步IO模型图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/3.png)
采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。
### 1.3 代码示例
下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。代码示例出自闪电侠的博客,原地址如下:
[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a)
**客户端**
```java
/**
*
* @author 闪电侠
* @date 2018年10月14日
* @Description:客户端
*/
public class IOClient {
public static void main(String[] args) {
// TODO 创建多个线程,模拟多个客户端连接服务端
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}
```
**服务端**
```java
/**
* @author 闪电侠
* @date 2018年10月14日
* @Description: 服务端
*/
public class IOServer {
public static void main(String[] args) throws IOException {
// TODO 服务端处理客户端连接请求
ServerSocket serverSocket = new ServerSocket(3333);
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
new Thread(() -> {
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
}
}
```
### 1.4 总结
在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
## 2. NIO (New I/O)
### 2.1 NIO 简介
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
### 2.2 NIO的特性/NIO与IO区别
如果是在面试中回答这个问题,我觉得首先肯定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。如果,你把这些都回答上了我觉得你对于 NIO 就有了更为深入一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。
#### 1)Non-blocking IO(非阻塞IO)
**IO流是阻塞的,NIO流是不阻塞的。**
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
#### 2)Buffer(缓冲区)
**IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。**
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
#### 3)Channel (通道)
NIO 通过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
#### 4)Selector (选择器)
NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
![一个单线程中Selector维护3个Channel的示意图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Slector.png)
### 2.3 NIO 读数据和写数据方式
通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
数据读取和写入操作图示:
![NIO读写数据的方式](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/NIO读写数据的方式.png)
### 2.4 NIO核心组件简单介绍
NIO 包含下面几个核心的组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。我们上面已经对这三个概念进行了基本的阐述,这里就不多做解释了。
### 2.5 代码示例
代码示例出自闪电侠的博客,原地址如下:
[https://www.jianshu.com/p/a4e03835921a](https://www.jianshu.com/p/a4e03835921a)
客户端 IOClient.java 的代码不变,我们对服务端使用 NIO 进行改造。以下代码较多而且逻辑比较复杂,大家看看就好。
```java
/**
*
* @author 闪电侠
* @date 2019年2月21日
* @Description: NIO 改造后的服务端
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程,
// 而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等
Selector serverSelector = Selector.open();
// 2. clientSelector负责轮询连接是否有数据可读
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(3333));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set set = serverSelector.selectedKeys();
Iterator keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set set = clientSelector.selectedKeys();
Iterator keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(
Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
```
为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?从上面的代码中大家都可以看出来,是真的难用!除了编程复杂、编程模型难之外,它还有以下让人诟病的问题:
- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug
Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。
### 3. AIO (Asynchronous I/O)
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:[《漫话:如何给女朋友解释什么是Linux的五种IO模型?》](https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect) )
查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
## 参考
- 《Netty 权威指南》第二版
- https://zhuanlan.zhihu.com/p/23488863 (美团技术团队)
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
- [Servlet总结](#servlet总结)
- [阐述Servlet和CGI的区别?](#阐述servlet和cgi的区别)
- [CGI的不足之处:](#cgi的不足之处)
- [Servlet的优点:](#servlet的优点)
- [Servlet接口中有哪些方法及Servlet生命周期探秘](#servlet接口中有哪些方法及servlet生命周期探秘)
- [get和post请求的区别](#get和post请求的区别)
- [什么情况下调用doGet\(\)和doPost\(\)](#什么情况下调用doget和dopost)
- [转发(Forward)和重定向(Redirect)的区别](#转发forward和重定向redirect的区别)
- [自动刷新\(Refresh\)](#自动刷新refresh)
- [Servlet与线程安全](#servlet与线程安全)
- [JSP和Servlet是什么关系](#jsp和servlet是什么关系)
- [JSP工作原理](#jsp工作原理)
- [JSP有哪些内置对象、作用分别是什么](#jsp有哪些内置对象、作用分别是什么)
- [Request对象的主要方法有哪些](#request对象的主要方法有哪些)
- [request.getAttribute\(\)和 request.getParameter\(\)有何区别](#requestgetattribute和-requestgetparameter有何区别)
- [include指令include的行为的区别](#include指令include的行为的区别)
- [JSP九大内置对象,七大动作,三大指令](#jsp九大内置对象,七大动作,三大指令)
- [讲解JSP中的四种作用域](#讲解jsp中的四种作用域)
- [如何实现JSP或Servlet的单线程模式](#如何实现jsp或servlet的单线程模式)
- [实现会话跟踪的技术有哪些](#实现会话跟踪的技术有哪些)
- [Cookie和Session的的区别](#cookie和session的的区别)
## Servlet总结
在Java Web程序中,**Servlet**主要负责接收用户请求 `HttpServletRequest`,在`doGet()`,`doPost()`中做相应的处理,并将回应`HttpServletResponse`反馈给用户。**Servlet** 可以设置初始化参数,供Servlet内部使用。一个Servlet类只会有一个实例,在它初始化时调用`init()`方法,销毁时调用`destroy()`方法**。**Servlet需要在web.xml中配置(MyEclipse中创建Servlet会自动配置),**一个Servlet可以设置多个URL访问**。**Servlet不是线程安全**,因此要谨慎使用类变量。
## 阐述Servlet和CGI的区别?
### CGI的不足之处:
1,需要为每个请求启动一个操作CGI程序的系统进程。如果请求频繁,这将会带来很大的开销。
2,需要为每个请求加载和运行一个CGI程序,这将带来很大的开销
3,需要重复编写处理网络协议的代码以及编码,这些工作都是非常耗时的。
### Servlet的优点:
1,只需要启动一个操作系统进程以及加载一个JVM,大大降低了系统的开销
2,如果多个请求需要做同样处理的时候,这时候只需要加载一个类,这也大大降低了开销
3,所有动态加载的类可以实现对网络协议以及请求解码的共享,大大降低了工作量。
4,Servlet能直接和Web服务器交互,而普通的CGI程序不能。Servlet还能在各个程序之间共享数据,使数据库连接池之类的功能很容易实现。
补充:Sun Microsystems公司在1996年发布Servlet技术就是为了和CGI进行竞争,Servlet是一个特殊的Java程序,一个基于Java的Web应用通常包含一个或多个Servlet类。Servlet不能够自行创建并执行,它是在Servlet容器中运行的,容器将用户的请求传递给Servlet程序,并将Servlet的响应回传给用户。通常一个Servlet会关联一个或多个JSP页面。以前CGI经常因为性能开销上的问题被诟病,然而Fast CGI早就已经解决了CGI效率上的问题,所以面试的时候大可不必信口开河的诟病CGI,事实上有很多你熟悉的网站都使用了CGI技术。
参考:《javaweb整合开发王者归来》P7
## Servlet接口中有哪些方法及Servlet生命周期探秘
Servlet接口定义了5个方法,其中**前三个方法与Servlet生命周期相关**:
- `void init(ServletConfig config) throws ServletException`
- `void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException`
- `void destroy()`
- `java.lang.String getServletInfo()`
- `ServletConfig getServletConfig()`
**生命周期:** **Web容器加载Servlet并将其实例化后,Servlet生命周期开始**,容器运行其**init()方法**进行Servlet的初始化;请求到达时调用Servlet的**service()方法**,service()方法会根据需要调用与请求对应的**doGet或doPost**等方法;当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的**destroy()方法**。**init方法和destroy方法只会执行一次,service方法客户端每次请求Servlet都会执行**。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。
参考:《javaweb整合开发王者归来》P81
## get和post请求的区别
get和post请求实际上是没有区别,大家可以自行查询相关文章(参考文章:[https://www.cnblogs.com/logsharing/p/8448446.html](https://www.cnblogs.com/logsharing/p/8448446.html),知乎对应的问题链接:[get和post区别?](https://www.zhihu.com/question/28586791))!
可以把 get 和 post 当作两个不同的行为,两者并没有什么本质区别,底层都是 TCP 连接。 get请求用来从服务器上获得资源,而post是用来向服务器提交数据。比如你要获取人员列表可以用 get 请求,你需要创建一个人员可以用 post 。这也是 Restful API 最基本的一个要求。
推荐阅读:
- https://www.zhihu.com/question/28586791
- https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd
## 什么情况下调用doGet()和doPost()
Form标签里的method的属性为get时调用doGet(),为post时调用doPost()。
## 转发(Forward)和重定向(Redirect)的区别
**转发是服务器行为,重定向是客户端行为。**
**转发(Forward)**
通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response)方法实现的。RequestDispatcher可以通过HttpServletRequest 的getRequestDispatcher()方法获得。例如下面的代码就是跳转到login_success.jsp页面。
```java
request.getRequestDispatcher("login_success.jsp").forward(request, response);
```
**重定向(Redirect)** 是利用服务器返回的状态码来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过 `HttpServletResponse` 的 `setStatus(int status)` 方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。
1. **从地址栏显示来说**
forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址.
redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL.
2. **从数据共享来说**
forward:转发页面和转发到的页面可以共享request里面的数据.
redirect:不能共享数据.
3. **从运用地方来说**
forward:一般用于用户登陆的时候,根据角色转发到相应的模块.
redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等
4. 从效率来说
forward:高.
redirect:低.
## 自动刷新(Refresh)
自动刷新不仅可以实现一段时间之后自动跳转到另一个页面,还可以实现一段时间之后自动刷新本页面。Servlet中通过HttpServletResponse对象设置Header属性实现自动刷新例如:
```java
Response.setHeader("Refresh","5;URL=http://localhost:8080/servlet/example.htm");
```
其中5为时间,单位为秒。URL指定就是要跳转的页面(如果设置自己的路径,就会实现每过5秒自动刷新本页面一次)
## Servlet与线程安全
**Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。** 解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。
注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。
参考:《javaweb整合开发王者归来》P92
## JSP和Servlet是什么关系
其实这个问题在上面已经阐述过了,Servlet是一个特殊的Java程序,它运行于服务器的JVM中,能够依靠服务器的支持向浏览器提供显示内容。JSP本质上是Servlet的一种简易形式,JSP会被服务器处理成一个类似于Servlet的Java程序,可以简化页面内容的生成。Servlet和JSP最主要的不同点在于,Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。有人说,Servlet就是在Java中写HTML,而JSP就是在HTML中写Java代码,当然这个说法是很片面且不够准确的。JSP侧重于视图,Servlet更侧重于控制逻辑,在MVC架构模式中,JSP适合充当视图(view)而Servlet适合充当控制器(controller)。
## JSP工作原理
JSP是一种Servlet,但是与HttpServlet的工作方式不太一样。HttpServlet是先由源代码编译为class文件后部署到服务器下,为先编译后部署。而JSP则是先部署后编译。JSP会在客户端第一次请求JSP文件时被编译为HttpJspPage类(接口Servlet的一个子类)。该类会被服务器临时存放在服务器工作目录里面。下面通过实例给大家介绍。
工程JspLoginDemo下有一个名为login.jsp的Jsp文件,把工程第一次部署到服务器上后访问这个Jsp文件,我们发现这个目录下多了下图这两个东东。
.class文件便是JSP对应的Servlet。编译完毕后再运行class文件来响应客户端请求。以后客户端访问login.jsp的时候,Tomcat将不再重新编译JSP文件,而是直接调用class文件来响应客户端请求。
![JSP工作原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/1.png)
由于JSP只会在客户端第一次请求的时候被编译 ,因此第一次请求JSP时会感觉比较慢,之后就会感觉快很多。如果把服务器保存的class文件删除,服务器也会重新编译JSP。
开发Web程序时经常需要修改JSP。Tomcat能够自动检测到JSP程序的改动。如果检测到JSP源代码发生了改动。Tomcat会在下次客户端请求JSP时重新编译JSP,而不需要重启Tomcat。这种自动检测功能是默认开启的,检测改动会消耗少量的时间,在部署Web应用的时候可以在web.xml中将它关掉。
参考:《javaweb整合开发王者归来》P97
## JSP有哪些内置对象、作用分别是什么
[JSP内置对象 - CSDN博客 ](http://blog.csdn.net/qq_34337272/article/details/64310849 )
JSP有9个内置对象:
- request:封装客户端的请求,其中包含来自GET或POST请求的参数;
- response:封装服务器对客户端的响应;
- pageContext:通过该对象可以获取其他对象;
- session:封装用户会话的对象;
- application:封装服务器运行环境的对象;
- out:输出服务器响应的输出流对象;
- config:Web应用的配置对象;
- page:JSP页面本身(相当于Java程序中的this);
- exception:封装页面抛出异常的对象。
## Request对象的主要方法有哪些
- setAttribute(String name,Object):设置名字为name的request 的参数值
- getAttribute(String name):返回由name指定的属性值
- getAttributeNames():返回request 对象所有属性的名字集合,结果是一个枚举的实例
- getCookies():返回客户端的所有 Cookie 对象,结果是一个Cookie 数组
- getCharacterEncoding() :返回请求中的字符编码方式 = getContentLength() :返回请求的 Body的长度
- getHeader(String name) :获得HTTP协议定义的文件头信息
- getHeaders(String name) :返回指定名字的request Header 的所有值,结果是一个枚举的实例
- getHeaderNames() :返回所以request Header 的名字,结果是一个枚举的实例
- getInputStream() :返回请求的输入流,用于获得请求中的数据
- getMethod() :获得客户端向服务器端传送数据的方法
- getParameter(String name) :获得客户端传送给服务器端的有 name指定的参数值
- getParameterNames() :获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例
- getParameterValues(String name):获得有name指定的参数的所有值
- getProtocol():获取客户端向服务器端传送数据所依据的协议名称
- getQueryString() :获得查询字符串
- getRequestURI() :获取发出请求字符串的客户端地址
- getRemoteAddr():获取客户端的 IP 地址
- getRemoteHost() :获取客户端的名字
- getSession([Boolean create]) :返回和请求相关 Session
- getServerName() :获取服务器的名字
- getServletPath():获取客户端所请求的脚本文件的路径
- getServerPort():获取服务器的端口号
- removeAttribute(String name):删除请求中的一个属性
## request.getAttribute()和 request.getParameter()有何区别
**从获取方向来看:**
`getParameter()`是获取 POST/GET 传递的参数值;
`getAttribute()`是获取对象容器中的数据值;
**从用途来看:**
`getParameter()`用于客户端重定向时,即点击了链接或提交按扭时传值用,即用于在用表单或url重定向传值时接收数据用。
`getAttribute()` 用于服务器端重定向时,即在 sevlet 中使用了 forward 函数,或 struts 中使用了
mapping.findForward。 getAttribute 只能收到程序用 setAttribute 传过来的值。
另外,可以用 `setAttribute()`,`getAttribute()` 发送接收对象.而 `getParameter()` 显然只能传字符串。
`setAttribute()` 是应用服务器把这个对象放在该页面所对应的一块内存中去,当你的页面服务器重定向到另一个页面时,应用服务器会把这块内存拷贝另一个页面所对应的内存中。这样`getAttribute()`就能取得你所设下的值,当然这种方法可以传对象。session也一样,只是对象在内存中的生命周期不一样而已。`getParameter()`只是应用服务器在分析你送上来的 request页面的文本时,取得你设在表单或 url 重定向时的值。
**总结:**
`getParameter()`返回的是String,用于读取提交的表单中的值;(获取之后会根据实际需要转换为自己需要的相应类型,比如整型,日期类型啊等等)
`getAttribute()`返回的是Object,需进行转换,可用`setAttribute()`设置成任意对象,使用很灵活,可随时用
## include指令include的行为的区别
**include指令:** JSP可以通过include指令来包含其他文件。被包含的文件可以是JSP文件、HTML文件或文本文件。包含的文件就好像是该JSP文件的一部分,会被同时编译执行。 语法格式如下:
<%@ include file="文件相对 url 地址" %>
i**nclude动作:** ``动作元素用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。语法格式如下:
## JSP九大内置对象,七大动作,三大指令
[JSP九大内置对象,七大动作,三大指令总结](http://blog.csdn.net/qq_34337272/article/details/64310849)
## 讲解JSP中的四种作用域
JSP中的四种作用域包括page、request、session和application,具体来说:
- **page**代表与一个页面相关的对象和属性。
- **request**代表与Web客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个Web组件;需要在页面显示的临时数据可以置于此作用域。
- **session**代表与某个用户与服务器建立的一次会话相关的对象和属性。跟某个用户相关的数据应该放在用户自己的session中。
- **application**代表与整个Web应用程序相关的对象和属性,它实质上是跨越整个Web应用程序,包括多个页面、请求和会话的一个全局作用域。
## 如何实现JSP或Servlet的单线程模式
对于JSP页面,可以通过page指令进行设置。
`<%@page isThreadSafe="false"%>`
对于Servlet,可以让自定义的Servlet实现SingleThreadModel标识接口。
说明:如果将JSP或Servlet设置成单线程工作模式,会导致每个请求创建一个Servlet实例,这种实践将导致严重的性能问题(服务器的内存压力很大,还会导致频繁的垃圾回收),所以通常情况下并不会这么做。
## 实现会话跟踪的技术有哪些
1. **使用Cookie**
向客户端发送Cookie
```java
Cookie c =new Cookie("name","value"); //创建Cookie
c.setMaxAge(60*60*24); //设置最大时效,此处设置的最大时效为一天
response.addCookie(c); //把Cookie放入到HTTP响应中
```
从客户端读取Cookie
```java
String name ="name";
Cookie[]cookies =request.getCookies();
if(cookies !=null){
for(int i= 0;i
```
**优点:** Cookie被禁时可以使用
**缺点:** 所有页面必须是表单提交之后的结果。
4. HttpSession
在所有会话跟踪技术中,HttpSession对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的HttpSession。可以通过HttpServletRequest对象的getSession方 法获得HttpSession,通过HttpSession的setAttribute方法可以将一个值放在HttpSession中,通过调用 HttpSession对象的getAttribute方法,同时传入属性名就可以获取保存在HttpSession中的对象。与上面三种方式不同的 是,HttpSession放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的Servlet容器可以在内存将满时将HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。添加到HttpSession中的值可以是任意Java对象,这个对象最好实现了 Serializable接口,这样Servlet容器在必要的时候可以将其序列化到文件中,否则在序列化时就会出现异常。
## Cookie和Session的的区别
Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。
**Cookie 一般用来保存用户信息** 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。**Session 的主要作用就是通过服务端记录用户的状态。** 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- [IO流学习总结](#io流学习总结)
- [一 Java IO,硬骨头也能变软](#一-java-io,硬骨头也能变软)
- [二 java IO体系的学习总结](#二-java-io体系的学习总结)
- [三 Java IO面试题](#三-java-io面试题)
- [NIO与AIO学习总结](#nio与aio学习总结)
- [一 Java NIO 概览](#一-java-nio-概览)
- [二 Java NIO 之 Buffer\(缓冲区\)](#二-java-nio-之-buffer缓冲区)
- [三 Java NIO 之 Channel(通道)](#三-java-nio-之-channel(通道))
- [四 Java NIO之Selector(选择器)](#四-java-nio之selector(选择器))
- [五 Java NIO之拥抱Path和Files](#五-java-nio之拥抱path和files)
- [六 NIO学习总结以及NIO新特性介绍](#六-nio学习总结以及nio新特性介绍)
- [七 Java NIO AsynchronousFileChannel异步文件通](#七-java-nio-asynchronousfilechannel异步文件通)
- [八 高并发Java(8):NIO和AIO](#八-高并发java(8):nio和aio)
- [推荐阅读](#推荐阅读)
- [在 Java 7 中体会 NIO.2 异步执行的快乐](#在-java-7-中体会-nio2-异步执行的快乐)
- [Java AIO总结与示例](#java-aio总结与示例)
## IO流学习总结
### [一 Java IO,硬骨头也能变软](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483981&idx=1&sn=6e5c682d76972c8d2cf271a85dcf09e2&chksm=fd98542ccaefdd3a70428e9549bc33e8165836855edaa748928d16c1ebde9648579d3acaac10#rd)
**(1) 按操作方式分类结构图:**
![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png)
**(2)按操作对象分类结构图**
![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png)
### [二 java IO体系的学习总结](https://blog.csdn.net/nightcurtis/article/details/51324105)
1. **IO流的分类:**
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流。
2. **流的原理浅析:**
java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java Io流的40多个类都是从如下4个抽象类基类中派生出来的。
- **InputStream/Reader**: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- **OutputStream/Writer**: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
3. **常用的io流的用法**
### [三 Java IO面试题](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483985&idx=1&sn=38531c2cee7b87f125df7aef41637014&chksm=fd985430caefdd26b0506aa84fc26251877eccba24fac73169a4d6bd1eb5e3fbdf3c3b940261#rd)
## NIO与AIO学习总结
### [一 Java NIO 概览](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483956&idx=1&sn=57692bc5b7c2c6dfb812489baadc29c9&chksm=fd985455caefdd4331d828d8e89b22f19b304aa87d6da73c5d8c66fcef16e4c0b448b1a6f791#rd)
1. **NIO简介**:
Java NIO 是 java 1.4, 之后新出的一套IO接口NIO中的N可以理解为Non-blocking,不单纯是New。
2. **NIO的特性/NIO与IO区别:**
- 1)IO是面向流的,NIO是面向缓冲区的;
- 2)IO流是阻塞的,NIO流是不阻塞的;
- 3)NIO有选择器,而IO没有。
3. **读数据和写数据方式:**
- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
4. **NIO核心组件简单介绍**
- **Channels**
- **Buffers**
- **Selectors**
### [二 Java NIO 之 Buffer(缓冲区)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483961&idx=1&sn=f67bef4c279e78043ff649b6b03fdcbc&chksm=fd985458caefdd4e3317ccbdb2d0a5a70a5024d3255eebf38183919ed9c25ade536017c0a6ba#rd)
1. **Buffer(缓冲区)介绍:**
- Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels;
- Buffer本质上就是一块内存区;
- 一个Buffer有三个属性是必须掌握的,分别是:capacity容量、position位置、limit限制。
2. **Buffer的常见方法**
- Buffer clear()
- Buffer flip()
- Buffer rewind()
- Buffer position(int newPosition)
3. **Buffer的使用方式/方法介绍:**
- 分配缓冲区(Allocating a Buffer):
```java
ByteBuffer buf = ByteBuffer.allocate(28);//以ByteBuffer为例子
```
- 写入数据到缓冲区(Writing Data to a Buffer)
**写数据到Buffer有两种方法:**
1.从Channel中写数据到Buffer
```java
int bytesRead = inChannel.read(buf); //read into buffer.
```
2.通过put写数据:
```java
buf.put(127);
```
4. **Buffer常用方法测试**
说实话,NIO编程真的难,通过后面这个测试例子,你可能才能勉强理解前面说的Buffer方法的作用。
### [三 Java NIO 之 Channel(通道)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483966&idx=1&sn=d5cf18c69f5f9ec2aff149270422731f&chksm=fd98545fcaefdd49296e2c78000ce5da277435b90ba3c03b92b7cf54c6ccc71d61d13efbce63#rd)
1. **Channel(通道)介绍**
- 通常来说NIO中的所有IO都是从 Channel(通道) 开始的。
- NIO Channel通道和流的区别:
2. **FileChannel的使用**
3. **SocketChannel和ServerSocketChannel的使用**
4. **️DatagramChannel的使用**
5. **Scatter / Gather**
- Scatter: 从一个Channel读取的信息分散到N个缓冲区中(Buufer).
- Gather: 将N个Buffer里面内容按照顺序发送到一个Channel.
6. **通道之间的数据传输**
- 在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。
- transferFrom() :transferFrom方法把数据从通道源传输到FileChannel
- transferTo() :transferTo方法把FileChannel数据传输到另一个channel
### [四 Java NIO之Selector(选择器)](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483970&idx=1&sn=d5e2b133313b1d0f32872d54fbdf0aa7&chksm=fd985423caefdd354b587e57ce6cf5f5a7bec48b9ab7554f39a8d13af47660cae793956e0f46#rd)
1. **Selector(选择器)介绍**
- Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
- 使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。
2. **Selector(选择器)的使用方法介绍**
- Selector的创建
```java
Selector selector = Selector.open();
```
- 注册Channel到Selector(Channel必须是非阻塞的)
```java
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
```
- SelectionKey介绍
一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
- 从Selector中选择channel(Selecting Channels via a Selector)
选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中.
- 停止选择的方法
wakeup()方法 和close()方法。
3. **模板代码**
有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码。
4. **客户端与服务端简单交互实例**
### [五 Java NIO之拥抱Path和Files](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483976&idx=1&sn=2296c05fc1b840a64679e2ad7794c96d&chksm=fd985429caefdd3f48e2ee6fdd7b0f6fc419df90b3de46832b484d6d1ca4e74e7837689c8146&token=537240785&lang=zh_CN#rd)
**一 文件I/O基石:Path:**
- 创建一个Path
- File和Path之间的转换,File和URI之间的转换
- 获取Path的相关信息
- 移除Path中的冗余项
**二 拥抱Files类:**
- Files.exists() 检测文件路径是否存在
- Files.createFile() 创建文件
- Files.createDirectories()和Files.createDirectory()创建文件夹
- Files.delete()方法 可以删除一个文件或目录
- Files.copy()方法可以吧一个文件从一个地址复制到另一个位置
- 获取文件属性
- 遍历一个文件夹
- Files.walkFileTree()遍历整个目录
### [六 NIO学习总结以及NIO新特性介绍](https://blog.csdn.net/a953713428/article/details/64907250)
- **内存映射:**
这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。
### [七 Java NIO AsynchronousFileChannel异步文件通](http://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-asynchronousfilechannel.html)
Java7中新增了AsynchronousFileChannel作为nio的一部分。AsynchronousFileChannel使得数据可以进行异步读写。
### [八 高并发Java(8):NIO和AIO](http://www.importnew.com/21341.html)
## 推荐阅读
### [在 Java 7 中体会 NIO.2 异步执行的快乐](https://www.ibm.com/developerworks/cn/java/j-lo-nio2/index.html)
### [Java AIO总结与示例](https://blog.csdn.net/x_i_y_u_e/article/details/52223406)
AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。
**欢迎关注我的微信公众号:"Java面试通关手册"(一个有温度的微信公众号,期待与你共同进步~~~坚持原创,分享美文,分享各种Java学习资源):**
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
- [1. 面向对象和面向过程的区别](#1-面向对象和面向过程的区别)
- [2. Java 语言有哪些特点?](#2-java-语言有哪些特点)
- [3. 关于 JVM JDK 和 JRE 最详细通俗的解答](#3-关于-jvm-jdk-和-jre-最详细通俗的解答)
- [JVM](#jvm)
- [JDK 和 JRE](#jdk-和-jre)
- [4. Oracle JDK 和 OpenJDK 的对比](#4-oracle-jdk-和-openjdk-的对比)
- [5. Java和C++的区别?](#5-java和c的区别)
- [6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?](#6-什么是-java-程序的主类-应用程序和小程序的主类有何不同)
- [7. Java 应用程序与小程序之间有哪些差别?](#7-java-应用程序与小程序之间有哪些差别)
- [8. 字符型常量和字符串常量的区别?](#8-字符型常量和字符串常量的区别)
- [9. 构造器 Constructor 是否可被 override?](#9-构造器-constructor-是否可被-override)
- [10. 重载和重写的区别](#10-重载和重写的区别)
- [11. Java 面向对象编程三大特性: 封装 继承 多态](#11-java-面向对象编程三大特性-封装-继承-多态)
- [封装](#封装)
- [继承](#继承)
- [多态](#多态)
- [12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](#12-string-stringbuffer-和-stringbuilder-的区别是什么-string-为什么是不可变的)
- [13. 自动装箱与拆箱](#13-自动装箱与拆箱)
- [14. 在一个静态方法内调用一个非静态成员为什么是非法的?](#14-在一个静态方法内调用一个非静态成员为什么是非法的)
- [15. 在 Java 中定义一个不做事且没有参数的构造方法的作用](#15-在-java-中定义一个不做事且没有参数的构造方法的作用)
- [16. import java和javax有什么区别?](#16-import-java和javax有什么区别)
- [17. 接口和抽象类的区别是什么?](#17-接口和抽象类的区别是什么)
- [18. 成员变量与局部变量的区别有哪些?](#18-成员变量与局部变量的区别有哪些)
- [19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?](#19-创建一个对象用什么运算符对象实体与对象引用有何不同)
- [20. 什么是方法的返回值?返回值在类的方法里的作用是什么?](#20-什么是方法的返回值返回值在类的方法里的作用是什么)
- [21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?](#21-一个类的构造方法的作用是什么-若一个类没有声明构造方法该程序能正确执行吗-为什么)
- [22. 构造方法有哪些特性?](#22-构造方法有哪些特性)
- [23. 静态方法和实例方法有何不同](#23-静态方法和实例方法有何不同)
- [24. 对象的相等与指向他们的引用相等,两者有什么不同?](#24-对象的相等与指向他们的引用相等两者有什么不同)
- [25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?](#25-在调用子类构造方法之前会先调用父类没有参数的构造方法其目的是)
- [26. == 与 equals(重要)](#26--与-equals重要)
- [27. hashCode 与 equals (重要)](#27-hashcode-与-equals-重要)
- [hashCode()介绍](#hashcode介绍)
- [为什么要有 hashCode](#为什么要有-hashcode)
- [hashCode()与equals()的相关规定](#hashcode与equals的相关规定)
- [28. 为什么Java中只有值传递?](#28-为什么java中只有值传递)
- [29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?](#29-简述线程程序进程的基本概念以及他们之间关系是什么)
- [30. 线程有哪些基本状态?](#30-线程有哪些基本状态)
- [31 关于 final 关键字的一些总结](#31-关于-final-关键字的一些总结)
- [32 Java 中的异常处理](#32-java-中的异常处理)
- [Java异常类层次结构图](#java异常类层次结构图)
- [Throwable类常用方法](#throwable类常用方法)
- [异常处理总结](#异常处理总结)
- [33 Java序列化中如果有些字段不想进行序列化,怎么办?](#33-java序列化中如果有些字段不想进行序列化怎么办)
- [34 获取用键盘输入常用的两种方法](#34-获取用键盘输入常用的两种方法)
- [35 Java 中 IO 流](#35-java-中-io-流)
- [Java 中 IO 流分为几种?](#java-中-io-流分为几种)
- [既然有了字节流,为什么还要有字符流?](#既然有了字节流为什么还要有字符流)
- [BIO,NIO,AIO 有什么区别?](#bionioaio-有什么区别)
- [36. 常见关键字总结:static,final,this,super](#36-常见关键字总结staticfinalthissuper)
- [37. Collections 工具类和 Arrays 工具类常见方法总结](#37-collections-工具类和-arrays-工具类常见方法总结)
- [参考](#参考)
- [公众号](#公众号)
## 1. 面向对象和面向过程的区别
- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。**
- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。
参见 issue : [面向过程 :面向过程性能比面向对象高??](https://github.com/Snailclimb/JavaGuide/issues/431)
> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。
>
> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。
## 2. Java 语言有哪些特点?
1. 简单易学;
2. 面向对象(封装,继承,多态);
3. 平台无关性( Java 虚拟机实现平台无关性);
4. 可靠性;
5. 安全性;
6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
8. 编译与解释并存;
> 修正(参见: [issue#544](https://github.com/Snailclimb/JavaGuide/issues/544)):C++11开始(2011年的时候),C++就引入了多线程库,在windows、linux、macos都可以使用`std::thread`和`std::async`来创建线程。参考链接:http://www.cplusplus.com/reference/thread/thread/?kw=thread
## 3. 关于 JVM JDK 和 JRE 最详细通俗的解答
### JVM
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
**什么是字节码?采用字节码的好处是什么?**
> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
**Java 程序从源代码到运行一般有下面3步:**
![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png)
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
**总结:**
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
### JDK 和 JRE
JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。
JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
## 4. Oracle JDK 和 OpenJDK 的对比
可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么Oracle和OpenJDK之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。
对于Java 7,没什么关键的地方。OpenJDK项目主要基于Sun捐赠的HotSpot源代码。此外,OpenJDK被选为Java 7的参考实现,由Oracle工程师维护。关于JVM,JDK,JRE和OpenJDK之间的区别,Oracle博客帖子在2012年有一个更详细的答案:
> 问:OpenJDK存储库中的源代码与用于构建Oracle JDK的代码之间有什么区别?
>
> 答:非常接近 - 我们的Oracle JDK版本构建过程基于OpenJDK 7构建,只添加了几个部分,例如部署代码,其中包括Oracle的Java插件和Java WebStart的实现,以及一些封闭的源代码派对组件,如图形光栅化器,一些开源的第三方组件,如Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源Oracle JDK的所有部分,除了我们考虑商业功能的部分。
**总结:**
1. Oracle JDK大概每6个月发一次主要版本,而OpenJDK版本大概每三个月发布一次。但这不是固定的,我觉得了解这个没啥用处。详情参见:https://blogs.oracle.com/java-platform-group/update-and-faq-on-the-java-se-release-cadence。
2. OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全开源的;
3. Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;
4. 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;
5. Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
6. Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
## 5. Java和C++的区别?
我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来!
- 都是面向对象的语言,都支持封装、继承和多态
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理机制,不需要程序员手动释放无用内存
- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符‘\0’来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: https://blog.csdn.net/sszgg2006/article/details/49148189。
## 6. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?
一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。
## 7. Java 应用程序与小程序之间有哪些差别?
简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。
## 8. 字符型常量和字符串常量的区别?
1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符
2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节 (**注意: char在Java中占两个字节**)
> java编程思想第四版:2.2.2节
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg)
## 9. 构造器 Constructor 是否可被 override?
在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
## 10. 重载和重写的区别
#### 重载
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
下面是《Java核心技术》对重载这个概念的介绍:
![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/bg/desktopjava核心技术-重载.jpg)
#### 重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。**
## 11. Java 面向对象编程三大特性: 封装 继承 多态
### 封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
### 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
**关于继承如下 3 点请记住:**
1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。
2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
3. 子类可以用自己的方式实现父类的方法。(以后介绍)。
### 多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
## 12. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
**可变性**
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
AbstractStringBuilder.java
```java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
```
**线程安全性**
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
**性能**
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
**对于三者使用的总结:**
1. 操作少量的数据: 适用String
2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder
3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer
## 13. 自动装箱与拆箱
- **装箱**:将基本类型用它们对应的引用类型包装起来;
- **拆箱**:将包装类型转换为基本数据类型;
## 14. 在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
## 15. 在 Java 中定义一个不做事且没有参数的构造方法的作用
Java 程序在执行子类的构造方法之前,如果没有用 `super() `来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super() `来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
## 16. import java和javax有什么区别?
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。
所以,实际上java和javax没有区别。这都是一个名字。
## 17. 接口和抽象类的区别是什么?
1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。
3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。
5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。(详见issue:[https://github.com/Snailclimb/JavaGuide/issues/146](https://github.com/Snailclimb/JavaGuide/issues/146))
## 18. 成员变量与局部变量的区别有哪些?
1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
## 19. 创建一个对象用什么运算符?对象实体与对象引用有何不同?
new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。
## 20. 什么是方法的返回值?返回值在类的方法里的作用是什么?
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作!
## 21. 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么?
主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
## 22. 构造方法有哪些特性?
1. 名字与类名相同。
2. 没有返回值,但不能用void声明构造函数。
3. 生成类的对象时自动执行,无需调用。
## 23. 静态方法和实例方法有何不同
1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
## 24. 对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
## 25. 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?
帮助子类做初始化工作。
## 26. == 与 equals(重要)
**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
**举个例子:**
```java
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
```
**说明:**
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
## 27. hashCode 与 equals (重要)
面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”
### hashCode()介绍
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
### 为什么要有 hashCode
**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
### hashCode()与equals()的相关规定
1. 如果两个对象相等,则hashcode一定也是相同的
2. 两个对象相等,对两个对象分别调用equals方法都返回true
3. 两个对象有相同的hashcode值,它们也不一定是相等的
4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖**
5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
推荐阅读:[Java hashCode() 和 equals()的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html)
## 28. 为什么Java中只有值传递?
[为什么Java中只有值传递?](https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/MostCommonJavaInterviewQuestions/%E7%AC%AC%E4%B8%80%E5%91%A8%EF%BC%882018-8-7%EF%BC%89.md)
## 29. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
## 30. 线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4节)。
![Java线程的状态](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png)
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4节):
![Java线程状态变迁](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png)
由上图可以看出:
线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
> 操作系统隐藏 Java虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png)
当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。
## 31 关于 final 关键字的一些总结
final关键字主要用在三个地方:变量、方法、类。
1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。
## 32 Java 中的异常处理
### Java异常类层次结构图
![Java异常类层次结构图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-2/Exception.png)
在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。
**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。**
### Throwable类常用方法
- **public string getMessage()**:返回异常发生时的简要描述
- **public string toString()**:返回异常发生时的详细信息
- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息
### 异常处理总结
- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。
- **catch 块:** 用于处理try捕获到的异常。
- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return
语句时,finally语句块将在方法返回之前被执行。
**在以下4种特殊情况下,finally块不会被执行:**
1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行
2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行
3. 程序所在的线程死亡。
4. 关闭CPU。
下面这部分内容来自issue:。
**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下:
```java
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
```
如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。
## 33 Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
## 34 获取用键盘输入常用的两种方法
方法1:通过 Scanner
```java
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
```
方法2:通过 BufferedReader
```java
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
```
## 35 Java 中 IO 流
### Java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流。
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
按操作方式分类结构图:
![IO-操作方式分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作方式分类.png)
按操作对象分类结构图:
![IO-操作对象分类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/IO-操作对象分类.png)
### 既然有了字节流,为什么还要有字符流?
问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?**
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
### BIO,NIO,AIO 有什么区别?
- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
## 36. 常见关键字总结:static,final,this,super
详见笔主的这篇文章:
## 37. Collections 工具类和 Arrays 工具类常见方法总结
详见笔主的这篇文章:
### 38. 深拷贝 vs 浅拷贝
1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg)
## 参考
- https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre
- https://www.educba.com/oracle-vs-openjdk/
- https://stackoverflow.com/questions/22358071/differences-between-oracle-jdk-and-openjdk?answertab=active#tab-top
## 公众号
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- [1. 基础](#1-基础)
- [1.1. 正确使用 equals 方法](#11-正确使用-equals-方法)
- [1.2. 整型包装类值的比较](#12-整型包装类值的比较)
- [1.3. BigDecimal](#13-bigdecimal)
- [1.3.1. BigDecimal 的用处](#131-bigdecimal-的用处)
- [1.3.2. BigDecimal 的大小比较](#132-bigdecimal-的大小比较)
- [1.3.3. BigDecimal 保留几位小数](#133-bigdecimal-保留几位小数)
- [1.3.4. BigDecimal 的使用注意事项](#134-bigdecimal-的使用注意事项)
- [1.3.5. 总结](#135-总结)
- [1.4. 基本数据类型与包装数据类型的使用标准](#14-基本数据类型与包装数据类型的使用标准)
- [2. 集合](#2-集合)
- [2.1. Arrays.asList()使用指南](#21-arraysaslist使用指南)
- [2.1.1. 简介](#211-简介)
- [2.1.2. 《阿里巴巴Java 开发手册》对其的描述](#212-阿里巴巴java-开发手册对其的描述)
- [2.1.3. 使用时的注意事项总结](#213-使用时的注意事项总结)
- [2.1.4. 如何正确的将数组转换为ArrayList?](#214-如何正确的将数组转换为arraylist)
- [2.2. Collection.toArray()方法使用的坑&如何反转数组](#22-collectiontoarray方法使用的坑如何反转数组)
- [2.3. 不要在 foreach 循环里进行元素的 remove/add 操作](#23-不要在-foreach-循环里进行元素的-removeadd-操作)
# 1. 基础
## 1.1. 正确使用 equals 方法
Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
举个例子:
```java
// 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
String str = null;
if (str.equals("SnailClimb")) {
...
} else {
..
}
```
运行上面的程序会抛出空指针异常,但是我们把第二行的条件判断语句改为下面这样的话,就不会抛出空指针异常,else 语句块得到执行。:
```java
"SnailClimb".equals(str);// false
```
不过更推荐使用 `java.util.Objects#equals`(JDK7 引入的工具类)。
```java
Objects.equals(null,"SnailClimb");// false
```
我们看一下`java.util.Objects#equals`的源码就知道原因了。
```java
public static boolean equals(Object a, Object b) {
// 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
return (a == b) || (a != null && a.equals(b));
}
```
**注意:**
Reference:[Java中equals方法造成空指针异常的原因及解决方案](https://blog.csdn.net/tick_tock97/article/details/72824894)
- 每种原始类型都有默认值一样,如int默认值为 0,boolean 的默认值为 false,null 是任何引用类型的默认值,不严格的说是所有 Object 类型的默认值。
- 可以使用 == 或者 != 操作来比较null值,但是不能使用其他算法或者逻辑操作。在Java中`null == null`将返回true。
- 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
## 1.2. 整型包装类值的比较
所有整型包装类对象值的比较必须使用equals方法。
先看下面这个例子:
```java
Integer x = 3;
Integer y = 3;
System.out.println(x == y);// true
Integer a = new Integer(3);
Integer b = new Integer(3);
System.out.println(a == b);//false
System.out.println(a.equals(b));//true
```
当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。
**注意:**如果你的IDE(IDEA/Eclipse)上安装了阿里巴巴的p3c插件,这个插件如果检测到你用 ==的话会报错提示,推荐安装一个这个插件,很不错。
## 1.3. BigDecimal
### 1.3.1. BigDecimal 的用处
《阿里巴巴Java开发手册》中提到:**浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。** 具体原理和浮点数的编码方式有关,这里就不多提了,我们下面直接上实例:
```java
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999964
System.out.println(a == b);// false
```
具有基本数学知识的我们很清楚的知道输出并不是我们想要的结果(**精度丢失**),我们如何解决这个问题呢?一种很常用的方法是:**使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。**
```java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1
System.out.println(x.equals(y));// true
```
### 1.3.2. BigDecimal 的大小比较
`a.compareTo(b)` : 返回 -1 表示小于,0 表示 等于, 1表示 大于。
```java
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
System.out.println(a.compareTo(b));// 1
```
### 1.3.3. BigDecimal 保留几位小数
通过 `setScale`方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA会提示。
```java
BigDecimal m = new BigDecimal("1.255433");
BigDecimal n = m.setScale(3,BigDecimal.ROUND_HALF_DOWN);
System.out.println(n);// 1.255
```
### 1.3.4. BigDecimal 的使用注意事项
注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 **BigDecimal(String)** 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。
![《阿里巴巴Java开发手册》对这部分BigDecimal的描述](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/BigDecimal.png)
### 1.3.5. 总结
BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。
BigDecimal 的实现利用到了 BigInteger, 所不同的是 BigDecimal 加入了小数位的概念
## 1.4. 基本数据类型与包装数据类型的使用标准
Reference:《阿里巴巴Java开发手册》
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
比如我们如果自定义了一个Student类,其中有一个属性是成绩score,如果用Integer而不用int定义,一次考试,学生可能没考,值是null,也可能考了,但考了0分,值是0,这两个表达的状态明显不一样.
**说明** :POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
**正例** : 数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
**反例** : 比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。
# 2. 集合
## 2.1. Arrays.asList()使用指南
最近使用`Arrays.asList()`遇到了一些坑,然后在网上看到这篇文章:[Java Array to List Examples](http://javadevnotes.com/java-array-to-list-examples) 感觉挺不错的,但是还不是特别全面。所以,自己对于这块小知识点进行了简单的总结。
### 2.1.1. 简介
`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。
```java
String[] myArray = { "Apple", "Banana", "Orange" };
List myList = Arrays.asList(myArray);
//上面两个语句等价于下面一条语句
List myList = Arrays.asList("Apple","Banana", "Orange");
```
JDK 源码对于这个方法的说明:
```java
/**
*返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁,与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
*/
public static List asList(T... a) {
return new ArrayList<>(a);
}
```
### 2.1.2. 《阿里巴巴Java 开发手册》对其的描述
`Arrays.asList()`将数组转换为集合后,底层其实还是数组,《阿里巴巴Java 开发手册》对于这个方法有如下描述:
![阿里巴巴Java开发手-Arrays.asList()方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/阿里巴巴Java开发手-Arrays.asList()方法.png)
### 2.1.3. 使用时的注意事项总结
**传递的数组必须是对象数组,而不是基本类型。**
`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。
```java
int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1
```
当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组,这也就解释了上面的代码。
我们使用包装类型数组就可以解决这个问题。
```java
Integer[] myArray = { 1, 2, 3 };
```
**使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。**
```java
List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException
```
`Arrays.asList()` 方法返回的并不是 `java.util.ArrayList` ,而是 `java.util.Arrays` 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。
```java
List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList
```
下图是`java.util.Arrays$ArrayList`的简易源码,我们可以看到这个类重写的方法有哪些。
```java
private static class ArrayList extends AbstractList
implements RandomAccess, java.io.Serializable
{
...
@Override
public E get(int index) {
...
}
@Override
public E set(int index, E element) {
...
}
@Override
public int indexOf(Object o) {
...
}
@Override
public boolean contains(Object o) {
...
}
@Override
public void forEach(Consumer super E> action) {
...
}
@Override
public void replaceAll(UnaryOperator operator) {
...
}
@Override
public void sort(Comparator super E> c) {
...
}
}
```
我们再看一下`java.util.AbstractList`的`remove()`方法,这样我们就明白为啥会抛出`UnsupportedOperationException`。
```java
public E remove(int index) {
throw new UnsupportedOperationException();
}
```
### 2.1.4. 如何正确的将数组转换为ArrayList?
stackoverflow:https://dwz.cn/vcBkTiTW
**1. 自己动手实现(教育目的)**
```java
//JDK1.5+
static List arrayToList(final T[] array) {
final List l = new ArrayList(array.length);
for (final T s : array) {
l.add(s);
}
return (l);
}
```
```java
Integer [] myArray = { 1, 2, 3 };
System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList
```
**2. 最简便的方法(推荐)**
```java
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
```
**3. 使用 Java8 的Stream(推荐)**
```java
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
```
**4. 使用 Guava(推荐)**
对于不可变集合,你可以使用[`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java)类及其[`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101)与[`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225)工厂方法:(参数不能为空)
```java
List il = ImmutableList.of("string", "elements"); // from varargs
List il = ImmutableList.copyOf(aStringArray); // from array
```
对于可变集合,你可以使用[`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java)类及其[`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87)工厂方法:
```java
List l1 = Lists.newArrayList(anotherListOrCollection); // from collection
List l2 = Lists.newArrayList(aStringArray); // from array
List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs
```
**5. 使用 Apache Commons Collections**
```java
List list = new ArrayList();
CollectionUtils.addAll(list, str);
```
## 2.2. Collection.toArray()方法使用的坑&如何反转数组
该方法是一个泛型方法:` T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。
```java
String [] s= new String[]{
"dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List list = Arrays.asList(s);
Collections.reverse(list);
s=list.toArray(new String[0]);//没有指定类型的话会报错
```
由于JVM优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间,因为它只是为了说明返回的类型。详见:
## 2.3. 不要在 foreach 循环里进行元素的 remove/add 操作
如果要进行`remove`操作,可以调用迭代器的 `remove `方法而不是集合类的 remove 方法。因为如果列表在任何时间从结构上修改创建迭代器之后,以任何方式除非通过迭代器自身`remove/add`方法,迭代器都将抛出一个`ConcurrentModificationException`,这就是单线程状态下产生的 **fail-fast 机制**。
> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时,可能会抛出ConcurrentModificationException,单线程下也会出现这种情况,上面已经提到过。
`java.util`包下面的所有的集合类都是fail-fast的,而`java.util.concurrent`包下面的所有的类都是fail-safe的。
![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png)
## 泛型的实际应用
### 实现最小值函数
自己设计一个泛型的获取数组最小值的函数.并且这个方法只能接受Number的子类并且实现了Comparable接口。
```java
//注意:Number并没有实现Comparable
private static > T min(T[] values) {
if (values == null || values.length == 0) return null;
T min = values[0];
for (int i = 1; i < values.length; i++) {
if (min.compareTo(values[i]) > 0) min = values[i];
}
return min;
}
```
测试:
```java
int minInteger = min(new Integer[]{1, 2, 3});//result:1
double minDouble = min(new Double[]{1.2, 2.2, -1d});//result:-1d
String typeError = min(new String[]{"1","3"});//报错
```
## 数据结构
### 使用数组实现栈
**自己实现一个栈,要求这个栈具有`push()`、`pop()`(返回栈顶元素并出栈)、`peek()` (返回栈顶元素不出栈)、`isEmpty()`、`size()`这些基本的方法。**
提示:每次入栈之前先判断栈的容量是否够用,如果不够用就用`Arrays.copyOf()`进行扩容;
```java
public class MyStack {
private int[] storage;//存放栈中元素的数组
private int capacity;//栈的容量
private int count;//栈中元素数量
private static final int GROW_FACTOR = 2;
//TODO:不带初始容量的构造方法。默认容量为8
public MyStack() {
this.capacity = 8;
this.storage=new int[8];
this.count = 0;
}
//TODO:带初始容量的构造方法
public MyStack(int initialCapacity) {
if (initialCapacity < 1)
throw new IllegalArgumentException("Capacity too small.");
this.capacity = initialCapacity;
this.storage = new int[initialCapacity];
this.count = 0;
}
//TODO:入栈
public void push(int value) {
if (count == capacity) {
ensureCapacity();
}
storage[count++] = value;
}
//TODO:确保容量大小
private void ensureCapacity() {
int newCapacity = capacity * GROW_FACTOR;
storage = Arrays.copyOf(storage, newCapacity);
capacity = newCapacity;
}
//TODO:返回栈顶元素并出栈
private int pop() {
count--;
if (count == -1)
throw new IllegalArgumentException("Stack is empty.");
return storage[count];
}
//TODO:返回栈顶元素不出栈
private int peek() {
if (count == 0){
throw new IllegalArgumentException("Stack is empty.");
}else {
return storage[count-1];
}
}
//TODO:判断栈是否为空
private boolean isEmpty() {
return count == 0;
}
//TODO:返回栈中元素的个数
private int size() {
return count;
}
}
```
验证
```java
MyStack myStack = new MyStack(3);
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.push(5);
myStack.push(6);
myStack.push(7);
myStack.push(8);
System.out.println(myStack.peek());//8
System.out.println(myStack.size());//8
for (int i = 0; i < 8; i++) {
System.out.println(myStack.pop());
}
System.out.println(myStack.isEmpty());//true
myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty.
```
讲真的,下面推荐的文章或者资源建议阅读 3 遍以上。
### 团队
- **阿里巴巴Java开发手册(详尽版)**
- **Google Java编程风格指南:**
### 个人
- **程序员你为什么这么累:**
### 如何写出优雅的 Java 代码
1. 使用 IntelliJ IDEA 作为您的集成开发环境 (IDE)
1. 使用 JDK 8 或更高版本
1. 使用 Maven/Gradle
1. 使用 Lombok
1. 编写单元测试
1. 重构:常见,但也很慢
1. 注意代码规范
1. 定期联络客户,以获取他们的反馈
上述建议的详细内容:[八点建议助您写出优雅的Java代码](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485140&idx=1&sn=ecaeace613474f1859aaeed0282ae680&chksm=cea2491ff9d5c00982ffaece847ce1aead89fdb3fe190752d9837c075c79fc95db5940992c56&token=1328169465&lang=zh_CN&scene=21#wechat_redirect)。
更多代码优化相关内容推荐:
- [业务复杂=if else?刚来的大神竟然用策略+工厂彻底干掉了他们!](https://juejin.im/post/5dad23685188251d2c4ea2b6)
- [一些不错的 Java 实践!推荐阅读3遍以上!](http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4%BD%A0%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%86%99java%E5%90%97/)
- [[解锁新姿势] 兄dei,你代码需要优化了](https://juejin.im/post/5dafbc02e51d4524a0060bdd)
- [消灭 Java 代码的“坏味道”](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485599&idx=1&sn=d83ff4e6b1ee951a0a33508a10980ea3&chksm=cea24754f9d5ce426d18b435a8c373ddc580c06c7d6a45cc51377361729c31c7301f1bbc3b78&token=1328169465&lang=zh_CN#rd)
> ## 多线程系列文章
下列文章,我都更新在了我的博客专栏:[Java并发编程指南](https://blog.csdn.net/column/details/20860.html)。
1. [Java多线程学习(一)Java多线程入门](http://blog.csdn.net/qq_34337272/article/details/79640870)
2. [Java多线程学习(二)synchronized关键字(1)](http://blog.csdn.net/qq_34337272/article/details/79655194)
3. [Java多线程学习(二)synchronized关键字(2)](http://blog.csdn.net/qq_34337272/article/details/79670775)
4. [Java多线程学习(三)volatile关键字](http://blog.csdn.net/qq_34337272/article/details/79680771)
5. [Java多线程学习(四)等待/通知(wait/notify)机制](http://blog.csdn.net/qq_34337272/article/details/79690279)
6. [Java多线程学习(五)线程间通信知识点补充](http://blog.csdn.net/qq_34337272/article/details/79694226)
7. [Java多线程学习(六)Lock锁的使用](http://blog.csdn.net/qq_34337272/article/details/79714196)
8. [Java多线程学习(七)并发编程中一些问题](https://blog.csdn.net/qq_34337272/article/details/79844051)
9. [Java多线程学习(八)线程池与Executor 框架](https://blog.csdn.net/qq_34337272/article/details/79959271)
> ## 多线程系列文章重要知识点与思维导图
### Java多线程学习(一)Java多线程入门
![](https://user-gold-cdn.xitu.io/2018/8/4/16504e0cb6bac32e?w=758&h=772&f=jpeg&s=247210)
### Java多线程学习(二)synchronized关键字(1)
![](https://user-gold-cdn.xitu.io/2018/8/4/16504e245ceb3ea9?w=1028&h=490&f=jpeg&s=203811)
注意:**可重入锁的概念**。
另外要注意:**synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。** 如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。
### Java多线程学习(二)synchronized关键字(2)
![思维导图](https://user-gold-cdn.xitu.io/2018/8/4/16504e3d98213324?w=1448&h=439&f=jpeg&s=245012)
**注意:**
- 其他线程执行对象中**synchronized同步方法**(上一节我们介绍过,需要回顾的可以看上一节的文章)和**synchronized(this)代码块**时呈现同步效果;
- **如果两个线程使用了同一个“对象监视器”(synchronized(object)),运行结果同步,否则不同步**.
**synchronized关键字加到static静态方法**和**synchronized(class)代码块**上都是是给**Class类**上锁,而**synchronized关键字加到非static静态方法**上是给**对象**上锁。
数据类型String的常量池属性:**在Jvm中具有String常量池缓存的功能**
### Java多线程学习(三)volatile关键字
![volatile关键字](https://user-gold-cdn.xitu.io/2018/8/4/16504e4ab69d8d58)
**注意:**
**synchronized关键字**和**volatile关键字**比较
### Java多线程学习(四)等待/通知(wait/notify)机制
![本节思维导图](https://user-gold-cdn.xitu.io/2018/3/25/1625d2a9188ec021?w=1254&h=452&f=jpeg&s=229471)
### Java多线程学习(五)线程间通信知识点补充
![本节思维导图](https://user-gold-cdn.xitu.io/2018/8/4/16504e618d6886c5?w=1146&h=427&f=jpeg&s=220573)
**注意:** ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
### Java多线程学习(六)Lock锁的使用
![本节思维导图](https://user-gold-cdn.xitu.io/2018/3/27/1626755a8e9a8774?w=1197&h=571&f=jpeg&s=258439)
### Java多线程学习(七)并发编程中一些问题
![思维导图](https://user-gold-cdn.xitu.io/2018/4/7/162a01b71ebc4842?w=1067&h=517&f=png&s=36857)
### Java多线程学习(八)线程池与Executor 框架
![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-14/86510659.jpg)