Java5 语法、集合框架等
泛型
枚举
装箱拆箱
变长参数
注解
foreach 循环
静态导入
格式化
线程框架/数据结构
Arrays 工具类/StringBuilder/instrument
泛型 不同于 C++中的泛型,Java 的泛型会在编译后被清除,这种机制被称为泛型擦除。
java 的类型推断基本都在编译期完成
优点 :可以免去大量的显式类型转换;缺点 :由于泛型擦除的存在,在很多场合下容易引起误会:
比如向 List类型的表里添加一个 String 类型对象就不会通过,因为在编译期间还需要进行类型检查。
在继承重写方法时,若父类中被重写的方法中含有泛型,因为泛型擦除理应变成重载,但是 Java 编译器会在编译后的字节码中添加桥方法(已经被类型擦除)、桥方法再调用重写的方法来解决;
泛型类型参数不能使用基本类型,因为基本类型不是 Object 的子类;
其他一些注意事项…
枚举 枚举可以使用 enum 声明,在 switch 中可以作为 case 后的标签。可以使用 EnumMap 来保存枚举到其他类型的映射或使用 EnumSet 保存枚举值的集合。优点 :
相对使用 int 或 String 当作枚举对象来说,Java 编译器本身提供了对 enum 的类型检查,可以更安全地使用;
可以用于声明单例对象。
装箱拆箱 基本类型可以自动转换成对应的包装类型,比如 boolean 会被包装为 Boolean。优点 :
缺点 :
注意拆箱时不能对 null 拆箱,不然会报空指针。
变长参数 可以传入任意多个相同类型的参数。优点 :
提供了更多灵活性,比如编写输出方法时可以格式化多个参数。
缺点 :
注解 注解需要和反射配合使用,JDK 提供了一些具有特定语义的注解: @Inherited:是否对类的子类继承的方法等起作用; @Target:作用目标; @Rentation:表示 annotation 是否保留在编译过的 class 文件中还是在运行时可读。
for/in 循环 优点 :
缺点 :
获取不到元素所在 index;
无法在遍历的时候删除元素;
静态 import 可以直接使用一个类中的静态方法。缺点 :
提供对日期、数字等的格式化支持
线程框架/数据结构
在线程中可以设置 UncaughtExceptionHandler,当抛出异常后可以执行指定的逻辑; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class ThreadingTest extends Thread { private int[] numbers; public ThreadingTest(int[] numbers) { setName("Simple Thread"); setUncaughtExceptionHandler( new SimpleThreadExceptionHandler()); this.numbers = numbers; } public void run() { int index = numbers.length; boolean finished = false; while (!finished) { index--; finished = true; for (int i = 0; i < index; i++) { // Create error condition if (numbers[i + 1] < 0) { throw new IllegalArgumentException( "Cannot pass negative numbers into this thread!"); } if (numbers[i] > numbers[i + 1]) { // swap int temp = numbers[i]; numbers[i] = numbers[i + 1]; numbers[i + 1] = temp; finished = false; } } } } public static void main(String[] args) { int[] numbers = new int[]{2, -1, 56, 4, 7}; ThreadingTest threadingTest = new ThreadingTest(numbers); threadingTest.start(); } } class SimpleThreadExceptionHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.err.printf("%s: %s at line %d of %s%n", t.getName(), e.toString(), e.getStackTrace()[0].getLineNumber(), e.getStackTrace()[0].getFileName()); } }
引入 Queue、BlockingQueue、ConcurrentMap 数据结构;
引入 JUC 线程池;
每次提交任务时,如果线程数还没达到 coreSize 就创建新线程并绑定该任务。 所以第 coreSize 次提交任务后线程总数必达到 coreSize,不会重用之前的空闲线程。
线程数达到 coreSize 后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用 take()从工作队列里拉活来干。
如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。
临时线程使用 poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。
如果 core 线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行 RejectExecutionHanlder。默认的 AbortPolicy 抛 RejectedExecutionException 异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。
Arrays 提供数组相关的一些工具类。 1 2 3 4 5 Arrays.sort(myArray); Arrays.toString(myArray) Arrays.binarySearch(myArray, 98) Arrays.deepToString(ticTacToe) Arrays.deepEquals(ticTacToe, ticTacToe3)
Override 支持协变 返回类型可以是父类中相应类型或其子类。
JVM JRE installer 能将一些系统 jar 文件加载到一种私有内部表示方式,然后转储到一个文件内,称为“shared archive”,下次启动应用的时候可以直接使用这个包内的类数据,这样可以减少部分启动时间。
如果机器至少有 2 CPUs 和至少 2GB 物理内存,use the Java HotSpot Server Virtual Machine (server VM) instead of the Java HotSpot Client Virtual Machine (client VM).,The aim is to improve performance even if no one configures the VM to reflect the application it’s running. In general, the server VM starts up more slowly than the client VM, but over time runs more quickly.
服务器类机器默认垃圾回收器改为并行垃圾回收器。 可以指定性能目标,并行收集器可以自动调整堆的大小,比如:
1 2 3 4 -XX:GCTimeLimit=time-limit :花费在GC上的时间上限,默认是98,当超过上限时,会抛出OutOfMemory(HeapSpace)的异常 -XX:GCHeapFreeLimit=space-limit :Heap空闲空间的最低比例下限,默认是2,当超过下限时,会抛出OutOfMemory(HeapSpace)的异常 -XX:MaxGCPauseMillis=nnn :最长的GC暂停时间,如果时间过长,会相应调整空间的大小(单位是毫秒) -XX:GCTimeRatio=nnn :最大的GC占总可用时间的比例,如果时间过长,会相应调整空间的大小(花费在GC上的时间比例不超过1 / (1 + nnn))
Thread 类中给出了三个线程优先级常量:
1 2 3 java.lang.Thread.MIN_PRIORITY = 1 java.lang.Thread.NORM_PRIORITY = 5 java.lang.Thread.MAX_PRIORITY = 10
默认情况下线程优先级为 java.lang.Thread.NORM_PRIORITY,我们可以自定义设置在[1..10]内。 JVM(Java HotSpot)将 Java 线程关联到唯一的一个 native thread。
网络编程(Socket) InetAddress IP 地址是在网络层封装上的,确定 Internet 上的一个唯一的地址,端口号是由传输层封装上的,标志主机上的一个服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class InetAddressTest { public static void main(String args[]){ InetAddressTest.printAddress(); } static void printAddress(){ try{ InetAddress address = InetAddress.getLocalHost(); System.out.println(address);//输出:机器名/IP地址 address = InetAddress.getByName("www.baidu.com"); System.out.println(address);//输出:域名/IP地址 InetAddress[] addresses = InetAddress.getAllByName("www.baidu.com"); for(InetAddress a : addresses){ System.out.println(a); } } catch(UnknownHostException e){ e.printStackTrace(); } } }
URL 和 UrlConnection UrlConnection 可以从一个 URL 中打开流,可以方便地进行 Http 数据的收发。 内部是使用 Socket 进行连接的。
1 2 3 4 5 6 7 8 9 // 获取链接属性 URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取 String protocal = url.getProtocol(); String host = url.getHost(); String file = url.getFile(); int port = url.getPort(); String ref = url.getRef();//获得#后面的 System.out.println(protocal + ", " + host + ", " + file + ", " + port + ", " + ref);
访问链接
1 2 3 4 5 6 7 8 9 10 // 访问链接读取数据 URL url = new URL("http://www.cnblogs.com/mengdd/archive/2013/03/09/2951877.html"); BufferedReader reader = new BufferedReader( new InputStreamReader(url.openStream())); String line; while((line = reader.readLine()) != null){ System.out.println(line); } reader.close();
下面是对 URLConnection 的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class URLTest { public static void main(String args[]){ URLTest c = new URLTest(); c.createURL(); c.printURLParam(); c.readURL(); c.printURL(); } void createURL(){ try{ URL url = new URL("http://localhost:8080/"); } catch(MalformedURLException e){ e.printStackTrace(); } } void printURLParam(){ try{ URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取 String protocal = url.getProtocol(); String host = url.getHost(); String file = url.getFile(); int port = url.getPort(); String ref = url.getRef();//获得#后面的 System.out.println(protocal + ", " + host + ", " + file + ", " + port + ", " + ref); } catch(MalformedURLException e){ e.printStackTrace(); } } void readURL(){//将网页内容拷贝到本地 try{ URL url = new URL("http://www.baidu.com"); URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream(); //或者直接is = url.openStream(); OutputStream os = new FileOutputStream("e:\\baidu.txt"); byte[] buffer = new byte[2048]; int length = 0; while((length = is.read(buffer, 0, buffer.length)) != -1){ os.write(buffer, 0, length); } is.close(); os.close(); } catch(MalformedURLException e){ e.printStackTrace(); } catch(IOException e){ e.printStackTrace(); } } void printURL(){//读取网页内容到控制台 try{ URL url = new URL("http://www.baidu.com"); BufferedReader reader = new BufferedReader( new InputStreamReader(url.openStream())); String line; while((line = reader.readLine()) != null){ System.out.println(line); } reader.close(); } catch(MalformedURLException e){ e.printStackTrace(); } catch(IOException e){ e.printStackTrace(); } } }
TCP 和 UDP 它们都是位于传输层的协议,为应用进程提供服务,根据不同的应用场景,会使用不同的协议。 TCP 是基于连接的、面向流的协议,提供可靠通信,因此每次通信必须先建立连接,建立连接后可以分多次进行传输任务,并且保证数据的正确性。 UDP 是基于无连接的、面向数据报的协议,提供不可靠通信,每次通信只需要发送一次数据报,可以分多次发送,但不保证能否到达、到达的顺序。
Socket 是 TCP 的应用编程接口,DatagramSocket 是 UDP 的应用编程接口,他们之间没有继承关系(都实现 Closeable 接口)。 Socket 使用时需要先指定目标主机地址和端口号,然后打开 io 流进行操作 1.服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 监听8080端口 ServerSocket server = new ServerSocket(8080); // 等待请求 Socket socket = server.accept(); // 进行通信 BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream())); String line = reader.readLine(); System.out.println(line); PrintWriter writer = new PrintWriter(socket.getOutputStream()); writer.println(line); writer.flush(); // 不要忘了这个 // 关闭资源 writer.close(); reader.close(); socket.close(); server.close();
2.客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 监听8080端口 Socket socket = new Socket("127.0.0.1", 8080); // 开始通信 PrintWriter writer = new PrintWriter(socket.getOutputStream()); writer.println("hello"); writer.flush(); BufferedReader reader = new BufferedReader( new InputStreamReader(socket.getInputStream())); String line = reader.readLine(); System.out.println(line); // 关闭资源 writer.close(); reader.close(); socket.close();
SocketChannel 和 ServerSocketChannel 缓冲(Buffer):相当于货物 管道(Channel):相当于配货车,支持同时装多件货物。 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。相当于中转站的分拣员。
使用 DatagramSocket 进行 UDP 通信 下面的代码使用 DatagramSocket 实现 UDP 通信 服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 创建服务器socket,指定端口 DatagramSocket socket = new DatagramSocket(7000); // 接收信息保存到一个缓冲区,DatagramPacket(bytes, len) byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer, 1024); socket.receive(packet); System.out.println(new String(buffer, 0, packet.getLength())); // 响应信息,接收时需要指定地址和端口 String str = "Welcome!"; DatagramPacket packet1 = new DatagramPacket(str.getBytes(), str.length(), packet.getAddress(), packet.getPort()); socket.send(packet1); // 关闭资源 socket.close();
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 DatagramSocket socket = new DatagramSocket(); // 发送数据包 String str = "Hello World"; DatagramPacket packet = new DatagramPacket(str.getBytes(), str.length(), InetAddress.getByName("localhost"), 7000); socket.send(packet); // 接收响应 byte[] buffer = new byte[1024]; DatagramPacket packet1 = new DatagramPacket(buffer, 100); socket.receive(packet1); System.out.println(new String(buffer, 0, packet1.getLength())); // 关闭资源 socket.close();
下面的代码使用 NIO 实现数据报协议 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // 发送 public void send() throws IOException{ // 打开数据报通道 DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); ByteBuffer buf = ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in); while(scan.hasNext()){ String str = scan.next(); buf.put((new Date().toString() + ":\n" + str).getBytes()); buf.flip(); dc.send(buf, new InetSocketAddress("127.0.0.1", 9898)); buf.clear(); } dc.close(); } // 接收 public void receive() throws IOException{ // 传送数据报通道 DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); dc.bind(new InetSocketAddress(9898)); Selector selector = Selector.open(); dc.register(selector, SelectionKey.OP_READ); while(selector.select() > 0){ Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while(it.hasNext()){ SelectionKey sk = it.next(); if(sk.isReadable()){ ByteBuffer buf = ByteBuffer.allocate(1024); dc.receive(buf); buf.flip(); System.out.println(new String(buf.array(), 0, buf.limit())); buf.clear(); } } it.remove(); } }
使用 NIO 实现简易 HttpServer 主要思路很简单: (1) 服务器打开后首先为 Selector 注册一个 OP_ACCEPT 的 key,这样 select 时就能接收客户端请求了; (2) 每接收一个请求后即为该 key 创建一个线程,处理该 key 的操作,操作包括 accept 和 read,对于前者,只需为该 key 的 selector 再注册一个 OP_READ 用于准备接下来的读请求; (3) 读取时先读入一个 Buffer,首先解析请求头部分,直到遇到一个空行结束,因为这里只考虑 GET 请求,所以不必继续解析请求体了; (4) 返回时,首先构建响应头,同样使用一个空行结束,然后构建响应体,写回客户端,结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public class HttpServer { public static void main(String[] args) throws IOException { // 监听8080端口 ServerSocketChannel server = ServerSocketChannel.open(); server.socket().bind(new InetSocketAddress(8080)); // 设置为非阻塞模式 server.configureBlocking(false); // 为server注册选择器 Selector selector = Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT); // 创建处理器 while(true) { // 等待请求,每次阻塞3s,若超过3s线程继续运行, // select(0)或select()表示一直阻塞 if(selector.select(3000) == 0) { continue; } // 获取所有待处理的选择键 Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator(); while(keyIter.hasNext()) { SelectionKey key = keyIter.next(); // 启动新线程以处理SelectionKey new Thread(new HttpHandler(key)).run(); // 处理完毕后,移除当前key keyIter.remove(); } } } private static class HttpHandler implements Runnable { private int bufferSize = 1024; private String localCharset = "UTF-8"; private SelectionKey key; public HttpHandler(SelectionKey key) { this.key = key; } // 定义操作 private void handleAccept() throws IOException { // 接受请求后,注册OP_READ选择键以等待下一次请求 SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept(); clientChannel.configureBlocking(false); // !!!请求报文被限制在1024个字节内 clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize)); } private void handleRead() throws IOException { // 获取 SocketChannel sc = (SocketChannel) key.channel(); // 获取Buffer并重置 ByteBuffer buffer = (ByteBuffer) key.attachment(); buffer.clear(); // 读取,并判断内容是否为空,若是则关闭并退出 if(sc.read(buffer) == -1) { sc.close(); return; } // 接收请求数据 buffer.flip(); String receivedString = Charset.forName(localCharset).newDecoder(). decode(buffer).toString(); // 打印请求报文头 String[] requestMessage = receivedString.split("\r\n"); for(String s: requestMessage) { System.out.println(s); // 遇到空行说明报文头已经打印完 if(s.isEmpty()) { break; } } // 控制台打印首行信息 String[] firstLine = requestMessage[0].split(" "); System.out.println(); System.out.println("Method:\t" + firstLine[0]); System.out.println("url:\t" + firstLine[1]); System.out.println("HTTP Version:\t" + firstLine[2]); System.out.println(); // 返回客户端(!!!考虑对不同的Url和不同的请求方法进行不同的处理和响应) StringBuilder sendString = new StringBuilder(); sendString.append("HTTP/1.1 200 OK\r\n"); // 响应报文首行 sendString.append("Content-Type:text/html;charset=" + // !!!如果要传输流数据必须修改Content-Type localCharset + "\r\n"); sendString.append("\r\n"); // 报文结束后加一空行 // 响应体 sendString.append("<html><head><title>显示报文</title></head><body>"); sendString.append("接收到请求报文是: <br/>"); for(String s: requestMessage) { sendString.append(s + "<br/>"); } sendString.append("</body></html>"); // 使用缓冲区写入channel buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset)); sc.write(buffer); // 关闭资源 sc.close(); } public void run() { try { // 根据请求类型进行转发 if(key.isAcceptable()) { handleAccept(); } if(key.isReadable()) { handleRead(); } } catch (IOException e) { e.printStackTrace(); } } } }
使用 NIO-Selector 实现简易聊天室 客户端 声明数据结构
1 2 3 4 // 管道、选择器、字符集 private SocketChannel sc = null; private Selector selector = null; private Charset charset = Charset.forName("UTF-8");
创建线程类用于从服务端获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private class ClientThread extends Thread { public void run() { try { // 遍历所有选择键 while(selector.select() > 0) { for(SelectionKey sk : selector.selectedKeys()) { // 删除正在处理的 selector.selectedKeys().remove(sk); // 如果该键对应的通道中有可读的数据 if(sk.isReadable()) { // 使用缓冲区读取管道内的数据 SocketChannel sc = (SocketChannel) sk.channel(); ByteBuffer buff = ByteBuffer.allocate(1024); String content = ""; while(sc.read(buff) > 0) { buff.flip(); content += charset.decode(buff); } // 打印 System.out.println("聊天信息" + content); sk.interestOps(SelectionKey.OP_READ); } } } } catch(IOException e) { e.printStackTrace(); } } }
初始化
1 2 3 4 5 6 7 // 初始化SocketChannel InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999); sc = SocketChannel.open(isa); sc.configureBlocking(false); // 注册选择器 selector = Selector.open(); sc.register(selector, SelectionKey.OP_READ);
创建线程从服务端拉取数据,及不断从键盘读入发送到服务端
1 2 3 4 5 6 7 8 // 启动线程不断从服务端拉取 new ClientThread().start(); // 读取键盘输入到通道 Scanner reader = new Scanner(System.in); while(reader.hasNextLine()) { String line = reader.nextLine(); sc.write(charset.encode(line)); }
服务端 声明
1 2 3 // 选择器、字符集 private Selector selector = null; private Charset charset = Charset.forName("UTF-8");
初始化
1 2 3 4 5 6 7 8 // 打开管道 ServerSocketChannel server = ServerSocketChannel.open(); InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999); server.socket().bind(isa); server.configureBlocking(false); // 打开选择器 selector = Selector.open(); server.register(selector, SelectionKey.OP_ACCEPT);
接受连接,读取及发送数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 // 依次处理选择器上的选择键 while(selector.select() > 0) { for(SelectionKey sk : selector.selectedKeys()) { selector.selectedKeys().remove(sk); // 连接请求 if(sk.isAcceptable()) { SocketChannel sc = server.accept(); sc.configureBlocking(false); sc.register(selector, SelectionKey.OP_READ); sk.interestOps(SelectionKey.OP_ACCEPT); } // 存在可读取数据 if(sk.isReadable()) { // 使用缓冲区读取 SocketChannel sc = (SocketChannel) sk.channel(); ByteBuffer buff = ByteBuffer.allocate(1024); String content = ""; try { while(sc.read(buff) > 0) { buff.flip(); content += charset.decode(buff); } System.out.println("=======" + content); // 将管道设置为准备下一次读取 sk.interestOps(SelectionKey.OP_READ); } catch(IOException e) { // 如果该sk对应的管道出现异常,表明管道的客户端出现异常, // 所以从选择器中取消sk e.printStackTrace(); sk.cancel(); if(sk.channel() != null) { sk.channel().close(); } } // 说明聊天信息不为空 if(content.length() > 0) { // 将聊天信息输入每个选择键对应的管道中 for(SelectionKey key : selector.keys()) { Channel targetChannel = key.channel(); if(targetChannel instanceof SocketChannel) { SocketChannel dest = (SocketChannel) targetChannel; dest.write(charset.encode(content)); } } } } } }
QA
为什么 Socket 可以通过流来“持续地”读写,而 DatagramSocket 却只能一个一个数据报发哩? 这是由 TCP 和 UDP 的协议决定的,TCP 是面向流的协议,而 UDP 是面向数据报的协议。
可以用 TCP 客户端连接 UDP 服务器吗(或者反过来)? 不能,实验过确实不行,但是我还是心存疑惑,我猜测是因为接收方可以判断数据包的协议类型来确定是否接收。
socket 是怎么实现”全双工”的?
参考
Java Code Examples
Java 应用中的日志
错误处理的推荐实践
Java 开发中 10 个最为微妙的最佳编程实践
《Effective Java》
阿里巴巴编码规范(Java)
Java 泛型中的 PECS 原则
JNI
Program Library HOWTO (how to create and use program libraries on Linux)
Java and C/C++: JNI Guide
Java5
Java5 的新特性
New Features and Enhancements J2SE 5.0
java 泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
网络
HttpClient 使用详解
基于 JavaMail 的 Java 邮件发送:简单邮件发送
qq 邮箱服务器地址
工程
IntelliJ IDEA 使用教程(2019 图文版) – 从入门到上瘾