为了账号安全,请及时绑定邮箱和手机立即绑定
4. Java ServerSocket 类分析

Java 语言抽象了 java.net.ServerSocket 类表示服务器监听 Socket,此类只用在服务器端,通过调用它的 accept 方法来获取新的连接。accept 方法的返回值是 java.net.Socket 类型,后续服务器和客户端的数据收发,都是通过 accept 方法返回的 Socket 对象完成。java.net.ServerSocket 类也提供了一组重载的构造方法,方便程序员选择。 public ServerSocket(int port) throws BindException, IOException public ServerSocket(int port, int queueLength) throws BindException, IOException public ServerSocket(int port, int queueLength, InetAddress bindAddress) throws IOException public ServerSocket() throws IOExceptionport 参数用于传入服务器监听的端口号。如果传入的 port 是 0,系统会随机选择一个端口监听。queueLength 参数用于设置连接接收队列的长度。不传入此参数,采用系统默认长度。bindAddress 参数用于将监听 Socket 绑定到一个本地接口。如果传入此参数,服务器会监听指定的接口地址;如果不指定此参数,默认会监听通配符 IP 地址,比如 IPv4 是 0.0.0.0。提示:可以通过 netstat 命令查看服务器程序监听的 IP 地址和端口号。如果你是通过无参构造方法构造 java.net.ServerSocket 类的对象,需要手动调用它的 bind 方法,绑定监听端口和接口地址。创建一个简单的服务器监听 Socket,示例代码如下:import java.io.*;import java.net.ServerSocket;import java.net.Socket;public class TCPServer { private static final int PORT =56002; public static void main(String[] args) { ServerSocket ss = null; try { // 创建一个服务器 Socket ss = new ServerSocket(PORT); // 监听新的连接请求 Socket conn = ss.accept(); System.out.println("Accept a new connection:" + conn.getRemoteSocketAddress().toString()); // 读取客户端数据 BufferedInputStream in = new BufferedInputStream(conn.getInputStream()); StringBuilder inMessage = new StringBuilder(); while(true){ int c = in.read(); if (c == -1 || c == '\n') break; inMessage.append((char)c); } System.out.println("Recv from client:" + inMessage.toString()); // 向客户端发送数据 String rsp = "Hello Client!\n"; BufferedOutputStream out = new BufferedOutputStream(conn.getOutputStream()); out.write(rsp.getBytes()); out.flush(); System.out.println("Send to client:" + rsp); conn.close(); } catch (IOException e) { e.printStackTrace(); } finally { if (ss != null){ try { ss.close(); } catch (IOException e) { e.printStackTrace(); } } } System.out.println("Server exit!"); }}我们创建的阻塞式服务端,所以 java.net.ServerSocket 的 accept 方法会阻塞线程,直到新连接返回。同样,在接收客户端的消息的时候注意消息边界的处理,最后向客户端发送响应的时候,需要调用 flush 方法。

Java 方法

本小节我们将学习什么是方法、如何自定义方法,并按照分类介绍每种方法的特点,对于有参数的方法传值,会讲到基本数据类型作为方法参数和引用数据类型作为方法参数的区别。也会学习可变参数方法的定义语法和使用场景,方法重载的使用和意义也是本节的重点学习内容。

Java 多态

本小节我们来学习面向对象的最后一大特征——多态。多态是面向对象最重要的特性。我们将介绍多态的概念和特点,并带领大家实现一个多态的案例,你将了解到多态的实现条件、什么是向上转型以及什么是向下转型,并学会使用instanceof运算符来检查对象引用是否是类型的实例。

Java 泛型

本小节我们将学习 Java5 以后出现的一个特性:泛型(Generics)。通过本小节的学习,你将了解到什么是泛型,为什么需要泛型,如何使用泛型,如何自定义泛型,类型通配符等知识。

6.1 Java 实现静态代理

在 Java 中实现静态代理还是比较简单,只要按照上述 UML 中分析角色规则来定义就能轻松实现。这里就用 Java 先去实现上述例子://IPurchaseHouse: 抽象买房接口interface IPurchaseHouse { void inquiryPrice();//询价 void visitHouse();//看房 void payDeposit();//付定金 void signAgreement();//签合同 void payMoney();//付钱 void getHouse();//拿房}//HouseOwner: 房子拥有者(房东)class HouseOwner implements IPurchaseHouse {//实现IPurchaseHouse共同接口 @Override public void inquiryPrice() { System.out.println("HouseOwner提出房子价格: 200W RMB"); } @Override public void visitHouse() { System.out.println("HouseOwner同意买房者来看房子"); } @Override public void payDeposit() { System.out.println("HouseOwner收了买房者1W RMB定金"); } @Override public void signAgreement() { System.out.println("HouseOwner与买房者签订合同"); } @Override public void payMoney() { System.out.println("买房者付钱给HouseOwner"); } @Override public void getHouse() { System.out.println("买房者拿到房子"); }}//HouseAgent: 房产中介class HouseAgent implements IPurchaseHouse { private IPurchaseHouse mHouseOwner;//具体房东HouseOwner被代理对象引用 public HouseAgent(IPurchaseHouse houseOwner) { mHouseOwner = houseOwner; } @Override public void inquiryPrice() { mHouseOwner.inquiryPrice();//通过具体房东HouseOwner引用去调用inquiryPrice } @Override public void visitHouse() { mHouseOwner.visitHouse();//通过具体房东HouseOwner引用去调用visitHouse } @Override public void payDeposit() { mHouseOwner.payDeposit();//通过具体房东HouseOwner引用去调用payDeposit } @Override public void signAgreement() { mHouseOwner.signAgreement();//通过具体房东HouseOwner引用去调用signAgreement } @Override public void payMoney() { mHouseOwner.payMoney();//通过具体房东HouseOwner引用去调用payMoney } @Override public void getHouse() { mHouseOwner.getHouse();//通过具体房东HouseOwner引用去调用getHouse }}//Client客户类class Client { public static void main(String[] args) { IPurchaseHouse houseOwner = new HouseOwner(); IPurchaseHouse houseAgent = new HouseAgent(houseOwner);//传入具体被代理类实例 houseAgent.inquiryPrice();//询问价格 houseAgent.visitHouse();//看房 houseAgent.payDeposit();//支付定金 houseAgent.signAgreement();//签合同 houseAgent.payMoney();//付钱 houseAgent.getHouse();//拿房 }}运行结果:HouseOwner提出房子价格: 200W RMBHouseOwner同意买房者来看房子HouseOwner收了买房者1W RMB定金HouseOwner与买房者签订合同买房者付钱给HouseOwner买房者拿到房子Process finished with exit code 0这就是静态代理具体的实现,可能有些并不能看到代理模式所带来的好处,看上去就像是代理类做了实际转发调用而已。实际上有个很明显优点就是: 可以在 HouseAgent 类中整个流程插入一些特有的操作或行为,而不会影响内部 HouseOwner 的实现,保护内部的实现。 还有一个优点就是代理类在保证 HouseOwner 核心功能同时可以扩展其他行为。上述结论可能有点抽象,假如现在有个不一样需求比如 A 房产中介,在看房之前首先得签订一个看房协议,但是这个协议只涉及购买用户与中介之间的协议。所以基于代理模式很轻松就实现。//修改后的HouseAgentAclass HouseAgentA implements IPurchaseHouse { private IPurchaseHouse mHouseOwner;//具体房东HouseOwner被代理对象引用 private boolean mIsSigned; public HouseAgentA(IPurchaseHouse houseOwner) { mHouseOwner = houseOwner; } @Override public void inquiryPrice() { mHouseOwner.inquiryPrice();//通过具体房东HouseOwner引用去调用inquiryPrice } @Override public void visitHouse() { if (mIsSigned) { System.out.println("您已经签订了看房协议,可以看房了"); mHouseOwner.visitHouse();//通过具体房东HouseOwner引用去调用visitHouse } else { System.out.println("很抱歉,您还没签订了看房协议,暂时不能看房"); } } public void signVisitHouseAgreement(boolean isSigned) { mIsSigned = isSigned; } @Override public void payDeposit() { mHouseOwner.payDeposit();//通过具体房东HouseOwner引用去调用payDeposit } @Override public void signAgreement() { mHouseOwner.signAgreement();//通过具体房东HouseOwner引用去调用signAgreement } @Override public void payMoney() { mHouseOwner.payMoney();//通过具体房东HouseOwner引用去调用payMoney } @Override public void getHouse() { mHouseOwner.getHouse();//通过具体房东HouseOwner引用去调用getHouse }}//Client客户类class Client { public static void main(String[] args) { IPurchaseHouse houseOwner = new HouseOwner(); IPurchaseHouse houseAgent = new HouseAgentA(houseOwner);//传入具体被代理类实例 houseAgent.inquiryPrice();//询问价格 ((HouseAgentA) houseAgent).signVisitHouseAgreement(true);//签订看房合同 houseAgent.visitHouse();//看房 houseAgent.payDeposit();//支付定金 houseAgent.signAgreement();//签合同 houseAgent.payMoney();//付钱 houseAgent.getHouse();//拿房 }}运行结果:HouseOwner提出房子价格: 200W RMB您已经签订了看房协议,可以看房了HouseOwner同意买房者来看房子HouseOwner收了买房者1W RMB定金HouseOwner与买房者签订合同买房者付钱给HouseOwner买房者拿到房子Process finished with exit code 0

3. Java Socket 类分析

Java 语言抽象了 java.net.Socket 类,表示一个 Socket,既可以用在客户端,又可以用在服务器端。其实 java.net.Socket 也是一个包装类,对外抽象了一组公共方法,具体实现是在 java.net.SocketImpl 类中完成的,它允许用户自定义具体实现。java.net.Socket 类包含的主要功能如下:创建 Socket,具体就是创建一个 java.net.Socket 类的对象。建立 TCP 连接,可以通过 java.net.Socket 类的构造方法完成,也可以调用它的 connect 方法完成。将 Socket 绑定到本地接口 IP 地址或者端口,可以调用 java.net.Socket 类的 bind 方法完成。提示:服务器需要做 bind 操作,客户端一般不需要做 bind 操作。关闭连接,可以调用 java.net.Socket 类的 close 方法完成。接收数据,可以通过 java.net.Socket 类的 getInputStream 方法,返回一个 java.io.InputStream 对象实现数据接收。发送数据,可以通过 java.net.Socket 类的 getOutputStream 方法,返回一个 java.io.OutputStream 对象实现数据发送。java.net.Socket 类提供了一组重载的构造方法,方便程序员选择,大体分为四类:可以传入服务器的 host 和 port 参数原型如下: public Socket(String host, int port) throws UnknownHostException, IOException public Socket(InetAddress address, int port) throws IOException对于 host 参数,你可以传入 IP 地址或者是域名。当然,你可以传入构造好的 InetAddress 地址结构。在 java.net.Socket 的构造方法中,首先会构造一个 InetAddress 地址结构,然后进行域名解析,最后调用它的 connect 方法和服务器建立连接。可以传入绑定的本地地址参数原型如下: public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException这类构造方法也可以传入 host 和 port 外,功能和上面类似。另外,还可以传入 localAddr 和 localPort,会调用 java.net.Socket 类的 bind 方法,绑定在本地的接口地址和端口。无参构造方法 public Socket()此构造方法,除了构造一个 java.net.Socket 类的对象,并不会去 connect 服务器。你需要调用它的 connect 方法连接服务器。public void connect(SocketAddress endpoint, int timeout) throws IOException自己调用 connect 方法,需要构造 SocketAddress 结构,当然你可以设置连接的超时时间,单位是毫秒(milliseconds)。访问代理服务器public Socket(Proxy proxy) 当你需要访问某个代理服务器时,可以调用此构造方法,Socket 会自动去连接代理服务器。创建一个简单的 java.net.Socket 客户端,示例代码如下:import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.IOException;import java.io.OutputStream;import java.net.InetSocketAddress;import java.net.Socket;import java.net.SocketAddress;public class TCPClient { // 服务器监听的端口号 private static final int PORT = 56002; private static final int TIMEOUT = 15000; public static void main(String[] args) { Socket client = null; try { // 在构造方法中传入 host 和 port // client = new Socket("192.168.43.49", PORT); // 调用无参构造方法 client = new Socket(); // 构造服务器地址结构 SocketAddress serverAddr = new InetSocketAddress("192.168.0.101", PORT); // 连接服务器,超时时间是 15 毫秒 client.connect(serverAddr, TIMEOUT); System.out.println("Client start:" + client.getLocalSocketAddress().toString()); // 向服务器发送数据 OutputStream out = new BufferedOutputStream(client.getOutputStream()); String req = "Hello Server!\n"; out.write(req.getBytes()); // 不能忘记 flush 方法的调用 out.flush(); System.out.println("Send to server:" + req); // 接收服务器的数据 BufferedInputStream in = new BufferedInputStream(client.getInputStream()); StringBuilder inMessage = new StringBuilder(); while(true){ int c = in.read(); if (c == -1 || c == '\n') break; inMessage.append((char)c); } System.out.println("Recv from server:" + inMessage.toString()); } catch (IOException e) { e.printStackTrace(); } finally { if (client != null){ try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } }}这里我们创建的是阻塞式的客户端,有几点需要注意的地方:通过 OutputStream 的对象向服务器发送完数据后,需要调用 flush 方法。BufferedInputStream 的 read 方法会阻塞线程,所以需要设计好消息边界的识别机制,示例代码是通过换行符 ‘\n’ 表示一个消息边界。java.net.Socket 的各个方法都抛出了 IOException 异常,需要捕获。注意调用 close 方法,关闭连接。

3.2 Java 5.0 之后自定义枚举类

在 Java 5.0 后,可以使用eunm关键字来定义一个枚举类,比较便捷,推荐大家使用这个方法来定义枚举类。通常需要以下几个步骤:使用enum关键字定义枚举类,这个类隐式继承自java.lang.Enum类;在枚举类内部,提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾;声明枚举类的属性和构造方法,在构造方法中为属性赋值;提供 getter 方法,由于Enum类重写了 toString()方法,因此一般不需要我们自己来重写。具体实例如下:/** * @author colorful@TaleLin */public class EnumDemo2 { public static void main(String[] args) { Sex male = Sex.MALE; // 打印 Sex 对象 System.out.println(male); // 打印 getter方法的值 System.out.println(male.getSexName()); System.out.println(Sex.FEMALE.getSexName()); System.out.println(Sex.UNKNOWN.getSexName()); }}/** * 使用 enum 关键字定义枚举类,默认继承自 Enum 类 */enum Sex { // 1.提供当前枚举类的多个对象,多个对象之间使用逗号分割,最后一个对象使用分号结尾 MALE("男"), FEMALE("女"), UNKNOWN("保密"); /** * 2.声明枚举类的属性 */ private final String sexName; /** * 3.编写构造方法,为属性赋值 */ Sex(String sexName) { this.sexName = sexName; } /** * 4.提供getter */ public String getSexName() { return sexName; }}运行结果:MALE男女保密

4. 基于 Java 代码示例及实现讲解

在说明斐波那契数列的递归描述之后,我们看看如何用 Java 代码来实现对斐波那契数列的计算。public class Fibonacci { public static void main(String[] args){ System.out.println(fibonacci(1)); System.out.println(fibonacci(2)); System.out.println(fibonacci(3)); System.out.println(fibonacci(4)); System.out.println(fibonacci(5)); } //斐波那契数列数列的计算 private static int fibonacci(int n){ //如果是终止条件,按照要求返回终止条件对应结果 if( n==1 || n==2 ){ return 1; }else { //非终止条件,按照要求把大的问题拆分成小问题,调用自身函数递归处理 return fibonacci(n-1)+fibonacci(n-2); } }}运行结果如下:11235代码中的第 4 行至第 8 行分别调用斐波那契数列计算函数,计算出斐波那契数列中对应 n=1,2,3,4,5 时斐波那契数列的取值,进行结果比较,判断斐波那契数列程序实现是否正确。代码中的第 12 行至第 20 行是斐波那契数列应用递归方法进行斐波那契数列的计算,按照递归的三要素进行计算处理。

4. Java NIO 服务器端实现步骤

因为服务端采用非阻塞模式,需要用到Java NIO 的 Selector 组件,这是 Java NIO 的 I/O 多路复用机制,可以同时监听多个 SocketChannel 是否有读写事件。创建 Java NIO 的 Selector 实例selector = Selector.open();打开服务器 ServerSocketChannelserverChannel = ServerSocketChannel.open();给 ServerSocketChannel 绑定监听的 socket 地址,监听 any_addr serverChannel.socket().bind(new InetSocketAddress(PORT));设置 SO_REUSEADDR 选项,作为服务器,这是基本的要求 serverChannel.socket().setReuseAddress(true);设置非阻塞模式,这是服务器的基本要求,也是本小节的重点 serverChannel.configureBlocking(false);向 Selector 注册 accept 事件 serverChannel.register(selector, SelectionKey.OP_ACCEPT, serverChannel);编写事件循环。所有需要读写数据的 SocketChannel,需要将读写事件注册到 Selector。调用 Selector 的 select 方法,调用线程会进入 I/O 事件监听状态。如果没有事件发生,调用线程会被阻塞,进入事件等待状态;如果有事件发生,Selector 的 select 方法会返回发生了 I/O 事件的 SocketChannel 个数。Selector 的 selectedKeys 方法返回一个 java.util.Set 类,集合中包含的是 SelectionKey 结构,SelectionKey 和 SocketChannel 是一一对应的,表示发生了 I/O 事件的 SocketChannel。所以需要 遍历 Set,分别处理每个 SelectionKey。 while (true) { int readyChannels = selector.select(); if (readyChannels == 0) { System.out.println("No socket has i/o events"); continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key != null) { } keyIterator.remove(); } }抽象一个内部类 Client,表示一个客户端连接,每当一个新连接建立的时候,创建一个此类的对象。private static class Client{ public void sendData(); public int recvData(); public void close();}通过 key.isAcceptable() 处理连接接收事件。if (key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. ServerSocketChannel ssc = (ServerSocketChannel) key.attachment(); SocketChannel newSock = ssc.accept(); newSock.configureBlocking(false); Client client = new Client(selector, newSock);} 通过 key.isReadable() 处理读事件if (key.isReadable()) { // a channel is ready for reading Client client = (Client) key.attachment(); int rc = client.recvData(); if (rc == 0) { client.sendData(); }}通过 key.isReadable() 处理“写”事件if (key.isWritable()) { // a channel is ready for writing Client client = (Client) key.attachment(); client.cancelEvent(SelectionKey.OP_WRITE); client.sendData();}服务器端完整代码如下:import java.io.*;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.*;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Iterator;import java.util.Set;public class NonblockTCPServer { // 服务器监听的端口 private final static int PORT = 9082; private Selector selector = null; private ServerSocketChannel serverChannel = null; private static class Client{ // 接收 buffer 长度 private final static int RECV_BUF_LEN = 1024; // 接收buffer 声明 private ByteBuffer recvBuff = null; // 发送 buffer 长度 private static final int SEND_BUFF_LEN = 1024; // 发送 buffer 声明 private ByteBuffer sendBuff = null; // the Selector private Selector selector = null; // SocketChannel 引用声明,表示一个连接 private SocketChannel socketChannel = null; private SelectionKey sk_ = null; private boolean canSend = true; public Client(Selector selector, SocketChannel newSock){ this.selector = selector; this.socketChannel = newSock; this.recvBuff = ByteBuffer.allocate(RECV_BUF_LEN); this.sendBuff = ByteBuffer.allocate(SEND_BUFF_LEN); this.register(SelectionKey.OP_READ); } private void register(int op){ try { if (sk_ == null){ sk_ = this.socketChannel.register(selector, op, this); } else { sk_.interestOps(op | sk_.interestOps()); } } catch (ClosedChannelException e) { e.printStackTrace(); } } public void cancelEvent(int ops){ if (sk_ == null) return; sk_.interestOps(sk_.interestOps() & (~ops)); } public void sendData() { try { int totalSendBytes = 0; String resp = null; if (canSend){ //设置日期格式 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); resp = "The server time : " + df.format(new Date()); sendBuff.putInt(resp.length()); sendBuff.put(resp.getBytes()); totalSendBytes = resp.length() + 4; sendBuff.flip(); }else { totalSendBytes = sendBuff.remaining(); } int sbytes = this.socketChannel.write(sendBuff); System.out.println("Send to client about message :" + resp); if (sbytes < totalSendBytes) { this.register(SelectionKey.OP_WRITE); canSend = false; } else { if (!canSend){ canSend = true; } sendBuff.rewind(); } } catch (IOException e) { e.printStackTrace(); } } public int recvData(){ try { int recvBytes = this.socketChannel.read(this.recvBuff); if (recvBytes < 0){ System.out.println("Meet error or the end of stream"); close(); return -1; }else if (recvBytes == 0){ return 0;// eagain } this.recvBuff.flip(); while (this.recvBuff.remaining() > 0) { // Incomplete message header if (this.recvBuff.remaining() < 4) { break; } int bodyLen = this.recvBuff.getInt(); if (bodyLen > this.recvBuff.remaining()) { // Incomplete message body break; } byte[] body = new byte[bodyLen]; this.recvBuff.get(body, 0, bodyLen); System.out.println("Recv message from client: " + new String(body, 0, bodyLen)); } // flip recv buffer this.recvBuff.compact(); return 0; } catch (IOException e) { e.printStackTrace(); close(); } return -1; } public void close(){ try { cancelEvent(SelectionKey.OP_WRITE | SelectionKey.OP_READ); if (this.socketChannel != null){ this.socketChannel.close(); } } catch (IOException e) { e.printStackTrace(); } } } public void start(){ try { selector = Selector.open(); serverChannel = ServerSocketChannel.open(); // 绑定监听的 socket 地址,监听 any_addr serverChannel.socket().bind(new InetSocketAddress(PORT)); // 设置 SO_REUSEADDR 选项,作为服务器,这是基本的要求 serverChannel.socket().setReuseAddress(true); // 设置非阻塞模式,作为服务器,也是基本要求 serverChannel.configureBlocking(false); // 注册 accept 事件 serverChannel.register(selector, SelectionKey.OP_ACCEPT, serverChannel); } catch (IOException e) { e.printStackTrace(); stop(); } } public void process() { try { while (true) { int readyChannels = selector.select(); if (readyChannels == 0) { System.out.println("No socket has i/o events"); continue; } Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key != null) { if (key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. ServerSocketChannel ssc = (ServerSocketChannel) key.attachment(); SocketChannel newSock = ssc.accept(); newSock.configureBlocking(false); Client client = new Client(selector, newSock); } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading Client client = (Client) key.attachment(); int rc = client.recvData(); if (rc == 0) { client.sendData(); } } else if (key.isWritable()) { // a channel is ready for writing Client client = (Client) key.attachment(); client.cancelEvent(SelectionKey.OP_WRITE); client.sendData(); } } keyIterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } public void stop(){ try { if (serverChannel != null){ serverChannel.close(); serverChannel = null; } if (selector != null) { selector.close(); selector = null; } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { NonblockTCPServer tcp = new NonblockTCPServer(); tcp.start(); tcp.process(); }}对于非阻塞式 Socket,需要处理发送 Buffer 满和接收 Buffer 为空的情况。服务器样例代码的 320 ~ 390 行的主要逻辑就是在处理非阻塞模式下,发送 Buffer 满和接收 Buffer 为空的逻辑。

3. Java NIO 客户端实现步骤

首先创建目标服务器地址结构,这和之前介绍的一致。 SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", PORT);通过 SocketChannel 的 open 方法打开一个客户端 Socket,参数是 SocketAddress 类型的对象。SocketChannel sock = null;sock = SocketChannel.open(serverAddr);注意:我们创建的是阻塞式客户端 Socket,如果需要创建非阻塞式客户端 Socket,需要调用sock.configureBlocking(false);创建用于收发数据的 ByteBuffer,分配 1024 字节大小的 buffer。 ByteBuffer recvBuff = ByteBuffer.allocate(1024); ByteBuffer sendBuff = ByteBuffer.allocate(1024);通过 SocketChannel 的 read 方法读取数据,读取的数据保存在 recvBuff 中,这是上面创建的 Bytebuffer 对象,长度是 1024 字节int rbytes = sock.read(recvBuff);SocketChannel 的 read 方法返回值如果是大于 0,表示读取的字节数;返回值如果是 -1,表示数据读取结束。对于非阻塞式 Socket,返回值可能是 0。通过 SocketChannel 的 write 方法向 Socket 写入数据,需要发送的数据,需要提前写入 ByteBuffer 中。sock.write(sendBuff);客户端完整代码:import java.io.IOException;import java.net.InetSocketAddress;import java.net.SocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SocketChannel;import java.text.SimpleDateFormat;import java.util.Date;public class NonblockTCPClient { // 服务器监听的端口 private final static int PORT = 9082; public static void main(String[] args) { SocketChannel sock = null; try { // 创建服务器地址结构 SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", PORT); sock = SocketChannel.open(serverAddr); ByteBuffer recvBuff = ByteBuffer.allocate(1024); ByteBuffer sendBuff = ByteBuffer.allocate(1024); int rquest_times = 10; while (true){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String request = "Request time"+ df.format(new Date()); sendBuff.putInt(request.length()); sendBuff.put(request.getBytes()); sendBuff.flip(); sock.write(sendBuff); System.out.println("Send request to server"); int bodyLen = -1; boolean isFlip = true; recvBuff.rewind(); while (true){ int rbytes = sock.read(recvBuff); if (rbytes == -1){ sock.close(); return; } if (bodyLen == -1){ if (rbytes < 4){ continue; } recvBuff.flip(); bodyLen = recvBuff.getInt(); isFlip =false; } if (isFlip ){ recvBuff.flip(); } if (recvBuff.remaining() < bodyLen){ recvBuff.compact(); continue; } byte[] body = new byte[bodyLen]; recvBuff.get(body); System.out.println("Recv server :" + new String(body, 0, bodyLen)); break; } if (rquest_times-- == 0) { break; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } sendBuff.rewind(); } } catch (IOException e) { e.printStackTrace(); try { if (sock != null){ sock.close(); } } catch (IOException e1) { e1.printStackTrace(); } } }}Java NIO API 同样会抛出 java.io.IOException 异常,需要 catch。TCP 数据接收逻辑是比较难处理的,100 ~ 130 行都是数据读取的逻辑处理,主要是进行协议解析,通过 length 字段识别一个完整的消息。

2.1 Java InputStream &amp; OutputStream

java.io.InputStream 类是一个抽象超类,它提供最小的编程接口和输入流的部分实现。java.io.InputStream 类定义的几类方法:读取字节或字节数组,一组 read 方法。标记流中的位置,mark 方法。跳过输入字节,skip 方法。找出可读取的字节数,available 方法。重置流中的当前位置,reset 方法。关闭流,close 方法。InputStream 流在创建实例时会自动打开,你可以调用 close 方法显式关闭流,也可以选择在垃圾回收 InputStream 时,隐式关闭流。需要注意的是垃圾回收机制关闭流,并不能立刻生效,可能会造成流对象泄漏,所以一般需要主动关闭。java.io.OutputStream 类同样是一个抽象超类,它提供最小的编程接口和输出流的部分实现。java.io.OutputStream 定义的几类方法:写入字节或字节数组,一组 write 方法。刷新流,flush 方法。关闭流,close 方法。OutputStream 流在创建时会自动打开,你可以调用 close 方法显式关闭流,也可以选择在垃圾回收 OutputStream 时,隐式关闭流。

4.2 Java 中有哪些关键字

关键字一律用小写字母标识,Java 语言中定义了如下表所示的关键字:关键字说明 abstract 表明类或者成员方法具有抽象属性 assert 断言,常用于程序的调试 boolean 基本数据类型:布尔类型 break 提前跳出一个块 byte 基本数据类型,字节类型 case 用在 switch 语句之中,表示其中的一个分支 catch 用在异常处理中,用来捕捉异常 char 基本数据类型:字符类型 class 用于声明一个类 const 保留关键字 continue 回到一个块的开始处 default 默认,用在 switch 语句中,表明一个默认的分支;JDK1.8 以后也作用于声明接口函数的默认实现 do 用在 do-while 循环结构中 double 基本数据类型:双精度浮点数类型 else 用在条件语句中,表明当条件不成立时的分支 enum 枚举 extends 表明一个类型是另一个类型的子类型。对于类,可以是另一个类或者抽象类;对于接口,可以是另一个接口 final 用来说明最终属性,表明一个类不能派生出子类,或者成员方法不能被覆盖,或者成员域的值不能被改变,用来定义常量 finally 用于处理异常情况,用来声明一个基本肯定会被执行到的语句块 float 基本数据类型之一,单精度浮点数类型 for 一种循环结构的引导词 goto 保留关键字,没有具体含义 if 条件语句的引导词 implements 表明一个类实现了给定的接口 import 表明要访问指定的类或包 instanceof 用来测试一个对象是否是指定类型的实例对象 int 基本数据类型之一,整数类型 interface 接口 long 基本数据类型之一,长整数类型 native 用来声明一个方法是由与计算机相关的语言(如 C/C++/FORTRAN 语言)实现的 new 用来创建新实例对象 package 包 private 一种访问控制方式:私用模式 protected 一种访问控制方式:保护模式 public 一种访问控制方式:共用模式 return 从成员方法中返回数据 short 基本数据类型之一,短整数类型 static 表明具有静态属性 strictfp 用来声明 FP_strict(单精度或双精度浮点数)表达式遵循 IEEE 754 算术规范 super 表明当前对象的父类型的引用或者父类型的构造方法 switch 分支语句结构的引导词 synchronized 表明一段代码需要同步执行 this 指向当前实例对象的引用 throw 抛出一个异常 throws 声明在当前定义的成员方法中所有需要抛出的异常 transient 声明不用序列化的成员域 try 尝试一个可能抛出异常的程序块 void 声明当前成员方法没有返回值 volatile 表明两个或者多个变量必须同步地发生变化 while 用在循环结构中

2.1 安装 Zookeeper 前置条件

在安装 Zookeeper 之前,我们需要在 Linux 环境中安装 Java 环境,我们这里使用的是 JDK 1.8.0_261,JDK 安装本小节不做演示。我们执行 java -version 命令就可以看见我们的 JDK 版本信息:java version "1.8.0_261"Java(TM) SE Runtime Environment (build 1.8.0_261-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)有了 Java 环境,我们就可以进行 Zookeeper 的安装了。

Java 循环语句

循环结构能够让程序员以最少的精力去完成大量重复的工作,它可以让计算机根据条件做循环计算,当条件成立时继续循环,当条件不成立时结束循环。依据前面所学的知识,如果我们想在屏幕上依次打印1到5,可以编写以下程序:483运行结果:12345但是这种编写代码的方案存在一些弊端:不灵活:如果我们想要更改需求,打印从6到10,那么就不得不逐行更改;难于维护:如果有大量代码,更容易产生bug;伸缩性差:依次打印从1到5貌似很简单,如果需求变为打印从1到100呢?这意味着需要编写大量的代码。使用循环结构,就可以解决上述弊端。下面我们打开代码编辑器,新建一个LoopPrintNums.java,复制如下代码:484运行结果:1 2 3 4 5 6 7 8 9 10 聪明的你可能发现,如果将i <= 10改为 i <= 100,屏幕将依次从1打印100。上述代码中,我们看到不需要再去编写多条打印语句,同样得到了我们想要的结果,代码量大大减少。那么上述代码具体有什么含义呢,为什么这样写就实现了多个数字的打印呢?在本小节中,我们就会进行详细介绍。

7. 小结

Java 并发理论基础是基于Java 的内存模型的,了解 Java 内存模型,能够更有助于后续对并发知识的理解和运用了。Java 的内存模型是并发原理的基础,在了解内存模型的基础上去理解共享内存和私有内存,了解不同内存状态以及 Java 线程的生命周期至关重要。

2.1 java中函数重载存在什么问题

无论是在 Java 或者 C++ 中都有函数重载一说,函数重载目的为了针对不同功能业务需求,然后暴露不同参数的接口,包括参数列表个数,参数类型,参数顺序。也就是说几乎每个不同需求都得一个函数来对应,随着以后的扩展,这个类中的相同名字函数会堆成山,而且每个函数之间又存在层级调用,函数与函数之间的参数列表差别有时候也是细微的,所以在调用方也会感觉很疑惑。举个例子(Android图片加载框架我们都习惯于再次封装一次,以便调用方便)fun ImageView.loadUrl(url: String) {//ImageView.loadUrl这个属于扩展函数,后期会介绍,暂时可以先忽略 loadUrl(Glide.with(context), url)}fun ImageView.loadUrl(requestManager: RequestManager, url: String) { loadUrl(requestManager, url, false)}fun ImageView.loadUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) { ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).start()}fun ImageView.loadUrl(urls: List<String>) { loadUrl(Glide.with(context), urls)}fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>) { loadUrl(requestManager, urls, false)}fun ImageView.loadUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) { ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).start()}fun ImageView.loadRoundUrl(url: String) { loadRoundUrl(Glide.with(context), url)}fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String) { loadRoundUrl(requestManager, url, false)}fun ImageView.loadRoundUrl(requestManager: RequestManager, url: String, isCrossFade: Boolean) { ImageLoader.newTask(requestManager).view(this).url(url).crossFade(isCrossFade).round().start()}fun ImageView.loadRoundUrl(urls: List<String>) { loadRoundUrl(Glide.with(context), urls)}fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>) { loadRoundUrl(requestManager, urls, false)}fun ImageView.loadRoundUrl(requestManager: RequestManager, urls: List<String>, isCrossFade: Boolean) { ImageLoader.newTask(requestManager).view(this).url(urls).crossFade(isCrossFade).round().start()}//调用的地方activity.home_iv_top_banner.loadUrl(bannerUrl)activity.home_iv_top_portrait.loadUrl(portraitUrls)activity.home_iv_top_avatar.loadRoundUrl(avatarUrl)activity.home_iv_top_avatar.loadRoundUrl(avatarUrls)

5. 系统(System Application)类加载器

定义:系统类加载器是由 Sun 公司提供的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器。Tips:系统(System Application)类加载器加载的核心类库类型比较多,也会加载 lib 下的未被 BootStrap 类加载器加载的类库,还会加载 ext 文件夹下的未被 Extension 类加载器加载的类库,以及其他类库。总而言之一句话,加载除了 BootStrap 类加载器和 Extension 类加载器所加载的其余的所有的核心类库。示例:import java.net.URL;import java.net.URLClassLoader;public class LoaderDemo { public static void main(String[] args) { //取得应用(系统)类加载器 URLClassLoader appClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader(); System.out.println(appClassLoader); System.out.println("应用(系统)类加载器 的加载路径: "); URL[] urls = appClassLoader.getURLs(); for(URL url : urls) System.out.println(url); }}结果验证:运行 main 函数。应用(系统)类加载器 的加载路径: file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/charsets.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/deploy.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/javaws.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jce.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfr.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jfxswt.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/jsse.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/management-agent.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/plugin.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/resources.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/rt.jarfile:/E:/IdeaWorkspace/LeeCode/target/classes/file:/D:/Programs/IntelliJ%20IDEA%20Educational%20Edition%202019.3.1/lib/idea_rt.jar结果解析:我们可以看到, 系统(System Application)类加载器加载的类库种类很多,除了之前两种类加载器加载的类库,其余必须的核心类库,都由系统类加载器加载。

1. 包的声明和导入

在 Kotlin 中定义包与 Java 有点不同,在 Kotlin 中目录与包结构无需匹配,Kotlin 的源码可以在磁盘上的任意位置。与 Java 不一样的是 Kotlin 的文件是以 .kt 为后缀,而 Java 的文件是以 .java 为后缀。

2. 前提条件

因为 Eclipse 本身是使用 Java 编写的,所以,建议在下载和安装 Eclipse 之前,我们的操作系统上就安装有 Java 虚拟机。如果没有安装 Java 虚拟机,那么在后面的安装和运行 Eclipse 时会弹出相关的报错信息,如 virtual machine could not be found 或者 No Java virtual machine。不管是哪个操作系统弹出的提示信息都相似,以下是 Windows 系统中报错信息的示例:又或者:大家可以参考 Java 相关文档安装 Java 虚拟机,我们可以选择安装 JVM 或者 JDK。Tips: 一般来说,我们会直接安装 JDK。另外需要注意的是,目前 Eclipse 的版本需要的 Java 版本必须是 1.8 及以上的版本。

2.2 创建多行字符串

自Java 13 以后,我们可以使用三引号来表示一个多行字符串,被官方称为“文本块”。文本块常用来表示多行的或大段的文字。例如:public class StringTest3 { public static void main(String[] args) { String str = """ Java 很棒! 慕课网很棒!! 能够在慕课网学 Java 更棒!!!"""; System.out.println(str); }}Tips:这里需要注意的是,文本块中起始的三引号后面要另起一行,也就是说下面这种写法是错误的:String str = """Java 很棒! 慕课网很棒!! 能够在慕课网学 Java 更棒!!!""";如果我们直接使用javac命令编译代码,将会报错:javac StringTest3.javaStringTest3.java:4: 错误: 文本块 是预览功能,默认情况下禁用。 String str = """ ^ (请使用 --enable-preview 以启用 文本块)1 个错误报错告诉我们:文本块是预览功能,默认是禁用的。我们可以给编译器加上一些参数来编译执行代码:$ javac -source 14 --enable-preview StringTest3.java$ java --enable-preview StringTest3Java 很棒!慕课网很棒!!能够在慕课网学 Java 更棒!!!

2. JVM 定义

定义: JVM (Java Virtual Machine 简称 JVM),亦可称之为 Java 虚拟机。它是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,它是 Java 最具吸引力的特性之一。虚拟机:从字面意义上来理解,虚拟机是一个虚拟化出来的计算机。举个例子:我们经常在 Windows 操作系统上安装 Linux 的虚拟机,然后在 Linux 虚拟机上进行 Shell 脚本的编写练习,那么从这个角度上来说, Linux 虚拟机就类似于 JVM ,不同的是 Linux 虚拟机支撑了 Shell 脚本的运行环境,而 JVM 支撑了 Java 语言的运行。JVM 是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java 虚拟机屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

1.1 如何创建代码

在 java 目录上右击,在弹出的快捷菜单中点击 New > Java class:在向导弹出框中输出 Java 类的信息:Name:新建 Java 类的类名;Kind:新建 Java 类的类型:类、接口、枚举等;Superclass:新建 Java 类是否继承父类;Interface(s):新建 Java 类是否继承接口;Package:新建 Java 类的包名。例如:我想创建一个 SecondActivity 继承 Activity 父类和 OnClickListener 接口,填写信息如下:

4. 扩展(Extension)类加载器

定义:扩展类加载器是由 Sun 公司提供的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 %JAVA_HOME%/lib/ext 或者少数由系统变量 -Djava.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。Tips:此处我们依旧对大多数的核心类库加载位置进行讨论,即 %JAVA_HOME%/lib/ext 文件夹下的扩展核心类库。对于系统变量指定的类库,稍作了解即可。下边进行示例代码验证示例:import java.net.URL;import java.net.URLClassLoader;public class LoaderDemo { public static void main(String[] args) { //取得扩展类加载器 URLClassLoader extClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent(); System.out.println(extClassLoader); System.out.println("扩展类加载器 的加载路径: "); URL[] urls = extClassLoader.getURLs(); for(URL url : urls) System.out.println(url); }}结果验证:运行 main 函数。扩展类加载器 的加载路径: file:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/localedata.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunec.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jarfile:/D:/Programs/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar结果解析:我们可以看到,运行结果中所有的核心类库均来自 %JAVA_HOME%/lib/ext 的文件夹。

直播
查看课程详情
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号