Plusaber's Blog

  • Home

  • Tags

  • Categories

  • Archives

Java网络编程

Posted on 2014-08-02 | In java | Comments:

Java网络编程

Java的基本网络支持

使用InetAddress

Java提供了InteAddress类来代表IP地址,没有提供构造器,而是提供了如下两个静态方法来获取InetAddress实例。

  • getByName(String host)
  • getByAddress(byte[] addr)
  • getLocalHost,获取本机IP地址对应的实例。

获取信息:

  • String getCanonicalHostName()
  • String getHostAddress()
  • String getHostName()
  • boolean isReachable()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InetAddressTest
{
public static void main(String[] args)
throws Exception
{
// 根据主机名来获取对应的InetAddress实例
InetAddress ip = InetAddress.getByName("www.crazyit.org");
// 判断是否可达
System.out.println("crazyit是否可达:" + ip.isReachable(2000));
// 获取该InetAddress实例的IP字符串
System.out.println(ip.getHostAddress());
// 根据原始IP地址来获取对应的InetAddress实例
InetAddress local = InetAddress.getByAddress(
new byte[]{127,0,0,1});
System.out.println("本机是否可达:" + local.isReachable(5000));
// 获取该InetAddress实例对应的全限定域名
System.out.println(local.getCanonicalHostName());
}
}

使用URLDecoder和URLEncoder

普通字符串和application/x-www-form-urlencode MIME字符串的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class URLDecoderTest
{
public static void main(String[] args)
throws Exception
{

String keyWord = URLDecoder.decode(
"%B7%E8%BF%F1java", "GBK");
System.out.println(keyWord);

String urlStr = URLEncoder.encode(
"疯狂java讲义" , "GBK");
System.out.println(urlStr);
}
}

URL,URLConnection和URLPermission

URL对象代表统一资源定位器,是指向互联网资源指针。资源可以是简单的文件,也可是更加复杂的引用。通常URL有协议名、主机、端口和资源组成。
protocol:://host:port/resourceName

  • String getFile()
  • String getHost()
  • String getPath()
  • int getPort()
  • String getProtocol()
  • String getQuery()
  • URLConnection openConnection()
  • InputStream openStream()

一个简单的多线程下载工具类:

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
public class DownUtil
{
// 定义下载资源的路径
private String path;
// 指定所下载的文件的保存位置
private String targetFile;
// 定义需要使用多少线程下载资源
private int threadNum;
// 定义下载的线程对象
private DownThread[] threads;
// 定义下载的文件的总大小
private int fileSize;

public DownUtil(String path, String targetFile, int threadNum)
{
this.path = path;
this.threadNum = threadNum;
// 初始化threads数组
threads = new DownThread[threadNum];
this.targetFile = targetFile;
}

public void download() throws Exception
{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
+ "application/x-shockwave-flash, application/xaml+xml, "
+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
+ "application/x-ms-application, application/vnd.ms-excel, "
+ "application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Connection", "Keep-Alive");
// 得到文件大小
fileSize = conn.getContentLength();
conn.disconnect();
int currentPartSize = fileSize / threadNum + 1;
RandomAccessFile file = new RandomAccessFile(targetFile, "rw");
// 设置本地文件的大小
file.setLength(fileSize);
file.close();
for (int i = 0; i < threadNum; i++)
{
// 计算每条线程的下载的开始位置
int startPos = i * currentPartSize;
// 每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart = new RandomAccessFile(targetFile,
"rw");
// 定位该线程的下载位置
currentPart.seek(startPos);
// 创建下载线程
threads[i] = new DownThread(startPos, currentPartSize,
currentPart);
// 启动下载线程
threads[i].start();
}
}

// 获取下载的完成百分比
public double getCompleteRate()
{
// 统计多条线程已经下载的总大小
int sumSize = 0;
for (int i = 0; i < threadNum; i++)
{
sumSize += threads[i].length;
}
// 返回已经完成的百分比
return sumSize * 1.0 / fileSize;
}

private class DownThread extends Thread
{
// 当前线程的下载位置
private int startPos;
// 定义当前线程负责下载的文件大小
private int currentPartSize;
// 当前线程需要下载的文件块
private RandomAccessFile currentPart;
// 定义已经该线程已下载的字节数
public int length;

public DownThread(int startPos, int currentPartSize,
RandomAccessFile currentPart)
{
this.startPos = startPos;
this.currentPartSize = currentPartSize;
this.currentPart = currentPart;
}

@Override
public void run()
{
try
{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection)url
.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
+ "application/x-shockwave-flash, application/xaml+xml, "
+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
+ "application/x-ms-application, application/vnd.ms-excel, "
+ "application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty("Charset", "UTF-8");
InputStream inStream = conn.getInputStream();
// 跳过startPos个字节,表明该线程只下载自己负责哪部分文件。
inStream.skip(this.startPos);
byte[] buffer = new byte[1024];
int hasRead = 0;
// 读取网络数据,并写入本地文件
while (length < currentPartSize
&& (hasRead = inStream.read(buffer)) != -1)
{
currentPart.write(buffer, 0, hasRead);
// 累计该线程下载的总大小
length += hasRead;
}
currentPart.close();
inStream.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
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
public class MultiThreadDown
{
public static void main(String[] args) throws Exception
{
// 初始化DownUtil对象
final DownUtil downUtil = new DownUtil("http://www.crazyit.org/"
+ "attachment.php?aid=MTY0NXxjNjBiYzNjN3wxMzE1NTQ2MjU5fGNhO"
+ "DlKVmpXVmhpNGlkWmVzR2JZbnluZWpqSllOd3JzckdodXJOMUpOWWt0aTJz,"
, "oracelsql.rar", 4);
// 开始下载
downUtil.download();
new Thread()
{
public void run()
{
while(downUtil.getCompleteRate() < 1)
{
// 每隔0.1秒查询一次任务的完成进度,
// GUI程序中可根据该进度来绘制进度条
System.out.println("已完成:"
+ downUtil.getCompleteRate());
try
{
Thread.sleep(1000);
}
catch (Exception ex){}
}
}
}.start();
}
}

上面的程序还用到URLConnection和HttpURLConnection对象,其中前者表示应用程序和URL之间的通信连接,后者表示与URL之间的HTTP连接。程序可以功过URLConnection实例向该URL发送请求,读取URL引用的资源。

上面是直接获取输入流然后写到本地,连接操作都隐藏在openStream中了。

通常创建一个和URL的连接,并发送请求、读取此URL的资源需要如下几个步骤:

  1. 调用URL对象的openConnection()方法来创建URLConnection对象。
  2. 设置URLConnection的参数和普通请求属性
  3. 如果只是发送GET方式请求,则使用connect()方法建立和远程资源之间的实际连接即可;如果要发送POST方式的请求,则需要获取URLConnection实例对应的输出流来发送请求参数。
  4. 远程资源变为可用,程序可以访问远程资源的头字段或通过输入流读取远程资源的数据。在建立和远程资源的实际连接之前。程序可以通过如下方法来设置字段的值:
  • setAllowUserInteraction()
  • setDoInput()
  • setDoOutput()
  • setRequestProperty(String key, String value)

远程资源可用后:

  • Object getConnect()
  • String getHeaderField(String name)
  • getInputStream()
  • getOutputStream
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
ublic class GetPostTest
{
/**
* 向指定URL发送GET方法的请求
* @param url 发送请求的URL
* @param param 请求参数,格式满足name1=value1&name2=value2的形式。
* @return URL所代表远程资源的响应
*/
public static String sendGet(String url , String param)
{
String result = "";
String urlName = url + "?" + param;
try
{
URL realUrl = new URL(urlName);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent"
, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
// 建立实际的连接
conn.connect();
// 获取所有响应头字段
Map<String, List<String>> map = conn.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet())
{
System.out.println(key + "--->" + map.get(key));
}
try(
// 定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(
new InputStreamReader(conn.getInputStream() , "utf-8")))
{
String line;
while ((line = in.readLine())!= null)
{
result += "\n" + line;
}
}
}
catch(Exception e)
{
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
return result;
}
/**
* 向指定URL发送POST方法的请求
* @param url 发送请求的URL
* @param param 请求参数,格式应该满足name1=value1&name2=value2的形式。
* @return URL所代表远程资源的响应
*/
public static String sendPost(String url , String param)
{
String result = "";
try
{
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
try(
// 获取URLConnection对象对应的输出流
PrintWriter out = new PrintWriter(conn.getOutputStream()))
{
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
}
try(
// 定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader
(conn.getInputStream() , "utf-8")))
{
String line;
while ((line = in.readLine())!= null)
{
result += "\n" + line;
}
}
}
catch(Exception e)
{
System.out.println("发送POST请求出现异常!" + e);
e.printStackTrace();
}
return result;
}
// 提供主方法,测试发送GET请求和POST请求
public static void main(String args[])
{
// 发送GET请求
String s = GetPostTest.sendGet("http://localhost:8888/abc/a.jsp"
, null);
System.out.println(s);
// 发送POST请求
String s1 = GetPostTest.sendPost("http://localhost:8888/abc/login.jsp"
, "name=crazyit.org&pass=leegang");
System.out.println(s1);
}
}

基于TCP协议的网络编程

TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,从而在通信的两端形成网络虚拟链路。一旦建立了虚拟链路,两端的程序就可以通过虚拟链路通信。Java使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。

TCP协议基础

TCP协议负责信息包的按顺序传送,接收端收到后再将其正确还原。TCP通过重发以及确认机制保证准确传送,同时能够自适应网络的拥塞情况。

使用ServerSoket创建TCP服务器端

ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,则一直处于等待状态。

  • Socket accept(): 如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket。

构造器:

  • ServerSocket(int port)
  • ServerSocket(int prot, int backlog)
  • ServerSocket(int port, int backlog, InetAddress localAddr)

使用Socket进行通信

客户端可以使用Socket构造器来连接到指定的服务器:

  • Socket(InetAdress/String remoteAddress, int port)
  • Socket(InetAdress/String remoteAddress, int port, InetAddress localAddr, int localPort)

  • InputStream getInputStream()

  • OutputStream getOutputStream()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Server
{
public static void main(String[] args)
throws IOException
{
// 创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss = new ServerSocket(30000);
// 采用循环不断接受来自客户端的请求
while (true)
{
// 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = ss.accept();
// 将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
// 进行普通IO操作
ps.println("您好,您收到了服务器的新年祝福!");
// 关闭输出流,关闭Socket
ps.close();
s.close();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client
{
public static void main(String[] args)
throws IOException
{
Socket socket = new Socket("127.0.0.1" , 30000); //①
// 将Socket对应的输入流包装成BufferedReader
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// 进行普通IO操作
String line = br.readLine();
System.out.println("来自服务器的数据:" + line);
// 关闭输入流、socket
br.close();
socket.close();
}
}

可以设定超时时长:setSoTimeout(10000)

1
2
3
4
try {
Scanner scan = new Scanner(s.getInputStream());
String line = scan.nextLine();
} ...

另一种建立连接的方式:

1
2
Socket s = new Socket();
s.connect(new InetAddress(host, port), 10000);

加入多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyServer
{
//定义保存所有Socket的ArrayList
public static ArrayList<Socket> socketList
= new ArrayList<>();
public static void main(String[] args)
throws IOException
{
ServerSocket ss = new ServerSocket(30000);
while(true)
{
// 此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
// 每当客户端连接后启动一条ServerThread线程为该客户端服务
new Thread(new ServerThread(s)).start();
}
}
}
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
// 负责处理每个线程通信的线程类
public class ServerThread implements Runnable
{
// 定义当前线程所处理的Socket
Socket s = null;
// 该线程所处理的Socket所对应的输入流
BufferedReader br = null;
public ServerThread(Socket s)
throws IOException
{
this.s = s;
// 初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
public void run()
{
try
{
String content = null;
// 采用循环不断从Socket中读取客户端发送过来的数据
while ((content = readFromClient()) != null)
{
// 遍历socketList中的每个Socket,
// 将读到的内容向每个Socket发送一次
for (Socket s : MyServer.socketList)
{
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
// 定义读取客户端数据的方法
private String readFromClient()
{
try
{
return br.readLine();
}
// 如果捕捉到异常,表明该Socket对应的客户端已经关闭
catch (IOException e)
{
// 删除该Socket。
MyServer.socketList.remove(s); // ①
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyClient
{
public static void main(String[] args)throws Exception
{
Socket s = new Socket("127.0.0.1" , 30000);
// 客户端启动ClientThread线程不断读取来自服务器的数据
new Thread(new ClientThread(s)).start(); // ①
// 获取该Socket对应的输出流
PrintStream ps = new PrintStream(s.getOutputStream());
String line = null;
// 不断读取键盘输入
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
while ((line = br.readLine()) != null)
{
// 将用户的键盘输入内容写入Socket对应的输出流
ps.println(line);
}
}
}
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
public class ClientThread implements Runnable
{
// 该线程负责处理的Socket
private Socket s;
// 该线程所处理的Socket所对应的输入流
BufferedReader br = null;
public ClientThread(Socket s)
throws IOException
{
this.s = s;
br = new BufferedReader(
new InputStreamReader(s.getInputStream()));
}
public void run()
{
try
{
String content = null;
// 不断读取Socket输入流中的内容,并将这些内容打印输出
while ((content = br.readLine()) != null)
{
System.out.println(content);
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

半关闭的Socket

Socket提供半关闭方法,用来只关闭输入流或输出流,用以表示输出数据已经完成。

  • shutdownInput()
  • shutdownOutput()
  • isInputShutdown()
  • isOutputShutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Server
{
public static void main(String[] args)
throws Exception
{
ServerSocket ss = new ServerSocket(30000);
Socket socket = ss.accept();
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println("服务器的第一行数据");
ps.println("服务器的第二行数据");
//关闭socket的输出流,表明输出数据已经结束
socket.shutdownOutput();
//下面语句将输出false,表明socket还未关闭。
System.out.println(socket.isClosed());
Scanner scan = new Scanner(socket.getInputStream());
while (scan.hasNextLine())
{
System.out.println(scan.nextLine());
}
scan.close();
socket.close();
ss.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Client
{
public static void main(String[] args)
throws Exception
{
Socket s = new Socket("localhost" , 30000);
Scanner scan = new Scanner(s.getInputStream());
while (scan.hasNextLine())
{
System.out.println(scan.nextLine());
}
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println("客户端的第一行数据");
ps.println("客户端的第二行数据");
ps.close();
scan.close();
s.close();
}
}

使用NIO实现非阻塞Socket通信

使用Java 7的AIO实现非阻塞通信

基于UDP协议的网络编程

UDP协议是一种不可靠的网络协议,它在通信实例的两段各建立一个Socket,但这两个Socket之间并没有虚拟链路,只是发送接受数据报的对象。Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagamSocket发送、接收的数据报。

UDP协议基础

同前面TCP一样,UDP协议直接建立于IP协议之上。IP实际上是网络层协议,UDP和TCP为传输层协议。

UDP面向非连接,通信效率高,当可靠性不如TCP协议,传输大小限制64KB一下,不保证正确顺序。

使用DatagramSocket发送、接收数据

Java使用DatagramSocket对象作为基于UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,唯一作用是接收和发送数据报。Java使用DatagramPacket来代表数据报。

  • DatagramSocket()
  • DatagramSocket(int port)
  • DatagramSocket(int port, InetAddress laddr)

  • receive(DatagramPacket p)

  • send(DatagramPacket p)

DatagramSocket并不知道把数据发送到哪里,而是有DatagramPacket自身决定数据报的目的地。

DatagramPacket的构造器:

  • DatagramPacket(byte[] buf, int length) 接收数据
  • DatagramPacket(byte[] buf, int offset, int length)
  • DatagramPacket(byte[] buf, int length, InetAddress addr, int port) 发送数据
  • DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
1
2
3
4
5
DatagramPacket packet = new DatagramPacket(buf, 256)
socket.receive(packet); //等待数据报的到来

DatagramPacket packet = new DatagramPacket(buf, length, address, port);
socket.send(packet); //发送数据报
  • InetAddress getAddress()
  • int getPort()
  • SocketAddress getSocketAddress()
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
public class UdpServer
{
public static final int PORT = 30000;
// 定义每个数据报的最大大小为4K
private static final int DATA_LEN = 4096;
// 定义接收网络数据的字节数组
byte[] inBuff = new byte[DATA_LEN];
// 以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket =
new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket;
// 定义一个字符串数组,服务器发送该数组的的元素
String[] books = new String[]
{
"疯狂Java讲义",
"轻量级Java EE企业应用实战",
"疯狂Android讲义",
"疯狂Ajax讲义"
};
public void init()throws IOException
{
try(
// 创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket(PORT))
{
// 采用循环接受数据
for (int i = 0; i < 1000 ; i++ )
{
// 读取Socket中的数据,读到的数据放入inPacket封装的数组里。
socket.receive(inPacket);
// 判断inPacket.getData()和inBuff是否是同一个数组
System.out.println(inBuff == inPacket.getData());
// 将接收到的内容转成字符串后输出
System.out.println(new String(inBuff
, 0 , inPacket.getLength()));
// 从字符串数组中取出一个元素作为发送的数据
byte[] sendData = books[i % 4].getBytes();
// 以指定字节数组作为发送数据、以刚接受到的DatagramPacket的
// 源SocketAddress作为目标SocketAddress创建DatagramPacket。
outPacket = new DatagramPacket(sendData
, sendData.length , inPacket.getSocketAddress());
// 发送数据
socket.send(outPacket);
}
}
}
public static void main(String[] args)
throws IOException
{
new UdpServer().init();
}
}
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
public class UdpClient
{
// 定义发送数据报的目的地
public static final int DEST_PORT = 30000;
public static final String DEST_IP = "127.0.0.1";
// 定义每个数据报的最大大小为4K
private static final int DATA_LEN = 4096;
// 定义接收网络数据的字节数组
byte[] inBuff = new byte[DATA_LEN];
// 以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket =
new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket = null;
public void init()throws IOException
{
try(
// 创建一个客户端DatagramSocket,使用随机端口
DatagramSocket socket = new DatagramSocket())
{
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0] , 0
, InetAddress.getByName(DEST_IP) , DEST_PORT);
// 创建键盘输入流
Scanner scan = new Scanner(System.in);
// 不断读取键盘输入
while(scan.hasNextLine())
{
// 将键盘输入的一行字符串转换字节数组
byte[] buff = scan.nextLine().getBytes();
// 设置发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
// 发送数据报
socket.send(outPacket);
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
socket.receive(inPacket);
System.out.println(new String(inBuff , 0
, inPacket.getLength()));
}
}
}
public static void main(String[] args)
throws IOException
{
new UdpClient().init();
}
}

使用MulticastSocket实现多点广播

DatagramSocket只允许数据报发送给制定的目标地址,而MulticastSocket可以将数据报以广播形式发送到多个客户端。

若要使用多点广播,则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。IP多点广播实现了将单一信息发送到多个接受者的广播,其主要思想是设置一批特殊的网络地址作为多点广播地址,每个多点广播地址可以看为一个组,当需要发送,接收广播信息时,加入该组就可。当不需要时,就离开该组。

  • public MulticastSocket()
  • public MulticastSocket(int protNum)
  • public MulticastSocket(SocketAddress binaddr)

  • joinGroup(InetAddress multicastAddr): 将MulticastSocket加入一个组

  • leaveGroup(InetAddress multicastAddr): 将MulticastSocket离开一个组

  • setTimeToLive(int ttl),设置数据报最多可以跨过多少个网络。

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
public class MulticastSocketTest implements Runnable
{
// 使用常量作为本程序的多点广播IP地址
private static final String BROADCAST_IP
= "230.0.0.1";
// 使用常量作为本程序的多点广播目的的端口
public static final int BROADCAST_PORT = 30000;
// 定义每个数据报的最大大小为4K
private static final int DATA_LEN = 4096;
//定义本程序的MulticastSocket实例
private MulticastSocket socket = null;
private InetAddress broadcastAddress = null;
private Scanner scan = null;
// 定义接收网络数据的字节数组
byte[] inBuff = new byte[DATA_LEN];
// 以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket
= new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket = null;
public void init()throws IOException
{
try(
// 创建键盘输入流
Scanner scan = new Scanner(System.in))
{
// 创建用于发送、接收数据的MulticastSocket对象
// 由于该MulticastSocket对象需要接收数据,所以有指定端口
socket = new MulticastSocket(BROADCAST_PORT);
broadcastAddress = InetAddress.getByName(BROADCAST_IP);
// 将该socket加入指定的多点广播地址
socket.joinGroup(broadcastAddress);
// 设置本MulticastSocket发送的数据报会被回送到自身
socket.setLoopbackMode(false);
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0]
, 0 , broadcastAddress , BROADCAST_PORT);
// 启动以本实例的run()方法作为线程体的线程
new Thread(this).start();
// 不断读取键盘输入
while(scan.hasNextLine())
{
// 将键盘输入的一行字符串转换字节数组
byte[] buff = scan.nextLine().getBytes();
// 设置发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
// 发送数据报
socket.send(outPacket);
}
}
finally
{
socket.close();
}
}
public void run()
{
try
{
while(true)
{
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
socket.receive(inPacket);
// 打印输出从socket中读取的内容
System.out.println("聊天信息:" + new String(inBuff
, 0 , inPacket.getLength()));
}
}
// 捕捉异常
catch (IOException ex)
{
ex.printStackTrace();
try
{
if (socket != null)
{
// 让该Socket离开该多点IP广播地址
socket.leaveGroup(broadcastAddress);
// 关闭该Socket对象
socket.close();
}
System.exit(1);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
throws IOException
{
new MulticastSocketTest().init();
}
}

使用代理服务器

Java Exception

Posted on 2014-07-19 | In java | Comments:

Java异常

Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以强制程序必须处理所有的Checked异常,而Runtime异常无需处理。Checked异常可以提醒程序员需要处理所有可能发生的异常,但Checked异常也给编程带来了一些繁琐之处。

异常处理机制

当程序运行出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将”业务功能实现代码”和”错误处理代码”分离,提供更好的可读性。

使用try-catch捕获异常

1
2
3
4
5
try {
//业务实现代码
} catch(Exception e) {
//Exception处理代码
}

如果没有找到处理异常的catch块,程序就会直接退出。

异常类的继承体系

Java_exception_1

Java_exception_2

Error错误一般指与虚拟机相关的严重问题,应用程序通常无法处理,无需捕获。

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
public class DivTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException ie)
{
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch (NumberFormatException ne)
{
System.out.println("数字格式异常:程序只能接受整数参数");
}
catch (ArithmeticException ae)
{
System.out.println("算术异常");
}
catch (Exception e)
{
System.out.println("未知异常");
}
}
}

异常捕获时,一定要记住先捕获小异常,再捕获大异常

Java 7提供的多异常捕获

  • 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。
  • 捕获多种类型的异常时,异常变量有隐式的final修士,因此程序不能对异常变量重新赋值。
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
public class MultiExceptionTest
{
public static void main(String[] args)
{
try
{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException|NumberFormatException
|ArithmeticException ie)
{
System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
// 捕捉多异常时,异常变量默认有final修饰,
// 所以下面代码有错:
ie = new ArithmeticException("test"); //①
}
catch (Exception e)
{
System.out.println("未知异常");
// 捕捉一个类型的异常时,异常变量没有final修饰
// 所以下面代码完全正确。
e = new RuntimeException("test"); //②
}
}
}

访问异常信息

  • getMessage(): 返回该异常的详细描述字符串
  • printStackTrace(): 将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s): 将该异常的跟踪栈信息输出到制定输出流
  • getStackTrace(): 返回该异常的跟踪栈信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AccessExceptionMsg
{
public static void main(String[] args)
{
try
{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
}
}

使用finally回收资源

Java的垃圾回收机制不会回收任何物理资源(数据库连接,网络连接,磁盘文件等),这些物理资源都必须显式回收。

异常处理语法结构中只有try快是必须的,但catch和finally块至少出现其中之一,也可以同时出现,且finally快必须位于所有catch块之后。如果直接抛出异常就可以不需要catch和finally。

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
public class FinallyTest
{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
//return语句强制方法返回
return ; //①
//使用exit来退出虚拟机
//System.exit(1); //②
}
finally
{
//关闭磁盘文件,回收资源
if (fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收!");
}
}
}

除非在try块,catch块中调用了推出虚拟机的方法,否则不管在try,catch中执行了怎样的代码,是否抛出异常,是否return,异常处理的finally块总会被执行。

不要在finally块中使用如return或throw等导致方法终止的语句,一旦在finally块中使用了return或throw语句,会导致try块,catch块中的return,throw语句失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FinallyFlowTest
{
public static void main(String[] args)
throws Exception
{
boolean a = test();
System.out.println(a);
}
public static boolean test()
{
try
{
// 因为finally块中包含了return语句
// 所以下面的return语句失去作用
return true;
}
finally
{
return false;
}
}
}

当Java程序执行try块,catch块时遇到return或throw语句,这两个语句都会导致该块的执行立即结束,但系统并不会立即停止执行return或throw,而是先去寻找异常处理流程是否包含finally块,如果没有finally块,程序立即执行return或throw语句;如果有finally,系统立即执行finally块,只有finally块执行完成后,系统才会再次跳回来执行try块,cath块里的return或throw语句。如果finally块里也使用了return或throw,在方法在finally块就会终止,不会再执行try,catch中的return,throw代码。

异常处理的嵌套

最好不要使用超过两层的嵌套异常处理。

Java 7的自动关闭资源的try语句

为了保证try语句可以正常自动关闭资源,这些资源实现类必须实现AutoCloseable或者Closeable借口,也就是其中的close()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AutoCloseTest
{
public static void main(String[] args)
throws IOException
{
try (
// 声明、初始化两个可关闭的资源
// try语句会自动关闭这两个资源。
BufferedReader br = new BufferedReader(
new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new
FileOutputStream("a.txt")))
{
// 使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}

Checked异常和Runntime异常体系

Checked异常必须处理,两种处理方式:

  1. 当前方法明确如何处理该异常,使用try…catch块捕获异常,然后修复。
  2. 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

使用throws声明抛出异常

1
2
3
4
5
6
7
8
public class ThrowsTest
{
public static void main(String[] args)
throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}

一旦使用throws语句声明抛出该异常,程序就无需使用try…catch块来捕获该异常。

使用throws声明抛出异常时有一个限制,就是方法重写时”两小”中的一条规则:子类方法声明抛出的异常应该是父类方法声明抛出的异常的相同或子类,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OverrideThrows
{
public void test()throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows
{
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test()throws Exception
{
}
}

在大部分时候推荐使用Runntime异常,而不使用Checked异常。尤其当程序需要自行抛出异常时,使用Runntime异常将更加简洁。

当使用Runtime异常时,程序无需在方法中声明抛出Checked异常,一旦发生了自定义作物,程序只管抛出Runtime异常即可。

如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以使用try…catch块来捕获Runtime异常。

使用throw抛出异常

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
public class ThrowTest
{
public static void main(String[] args)
{
try
{
// 调用声明抛出Checked异常的方法,要么显式捕获该异常
// 要么在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
// 调用声明抛出Runtime异常的方法既可以显式捕获该异常,
// 也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
//自行抛出Exception异常
//该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
//自行抛出RuntimeException异常,既可以显式捕获该异常
//也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}

自定义异常类

在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常包含了该异常的有用信息,所以在选择抛出异常时,应该选择合适的异常类,从而可以明确描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。

用户自定义异常都应该继承Exception基类,如果希望自定义个Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息。(getMessage()).

1
2
3
4
5
6
7
8
9
10
public class AuctionException extends Exception
{
//无参数的构造器
public AuctionException(){} //①
//带一个字符串参数的构造器
public AuctionException(String msg) //②
{
super(msg);
}
}

如果需要自定义个Runtime异常,只需要将AuctionException.java程序中的Exception
基类改为RuntimeException基类,其他地方无需修改。

在大部分情况下,创建自定义异常都可采用与AuctionException.java相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常。

catch和throw同时使用

在更复杂的情况,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者再次捕获处理。

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
public class AuctionTest
{
private double initPrice = 30.0;
// 因为该方法中显式抛出了AuctionException异常,
// 所以此处需要声明抛出AuctionException异常
public void bid(String bidPrice)
throws AuctionException
{
double d = 0.0;
try
{
d = Double.parseDouble(bidPrice);
}
catch (Exception e)
{
// 此处完成本方法中可以对异常执行的修复处理,
// 此处仅仅是在控制台打印异常跟踪栈信息。
e.printStackTrace();
//再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,"
+ "不能包含其他字符!");
}
if (initPrice > d)
{
throw new AuctionException("竞拍价比起拍价低,"
+ "不允许竞拍!");
}
initPrice = d;
}
public static void main(String[] args)
{
AuctionTest at = new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
// 再次捕捉到bid方法中的异常。并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}

异常链

把原始异常信息隐藏起来,仅向上提必要的异常提示信息的处理方式,可以抱枕底层异常不会扩散到表现层,避免向上暴露太多的实现细节。

这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理方式,称为异常链。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SalException extends Exception
{
public SalException(){}
public SalException(String msg)
{
super(msg);
}
//创建一个可以接受Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出,开发者可以找到异常的源头,并跟踪异常一路触发的过程。

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
class SelfException extends RuntimeException
{
SelfException(){}
SelfException(String msg)
{
super(msg);
}
}
public class PrintStackTraceTest
{
public static void main(String[] args)
{
firstMethod();
}
public static void firstMethod()
{
secondMethod();
}
public static void secondMethod()
{
thirdMethod();
}
public static void thirdMethod()
{
throw new SelfException("自定义异常信息");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadExceptionTest implements Runnable
{
public void run()
{
firstMethod();
}
public void firstMethod()
{
secondMethod();
}
public void secondMethod()
{
int a = 5;
int b = 0;
int c = a / b;
}
public static void main(String[] args)
{
new Thread(new ThreadExceptionTest()).start();
}
}

异常处理规则

不要过度使用异常

对于完全已知的错误,应该编写处理这种错误的代码。只有对于外部的,不能确定和预支的运行时错误才使用异常。

异常处理的机制初衷是将不可预期的异常的处理代码和正常的业务逻辑处理代码分离,因此觉不要使用异常处理来代替正常的业务逻辑判断。

不要使用过于庞大的try块

正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。

避免使用Catch All语句

所谓catch all语句指的是一种异常捕获模块,它可以处理程序发生的所有可能异常。

不要忽略捕获到的异常

catch整个块为空,或者仅仅打印出出错信息都是不妥的。采取适当操作:

  1. 处理异常。如果知道如何处理,则采取相应措施
  2. 重新抛出新异常。把当前运行环境能做的做完,然后包装异常并抛出。
  3. 如果当前层不清楚如何处理,则不要在当前层使用catch语句,而是直接throws声明抛出该异常。

Ant基本使用

Posted on 2014-07-05 | In java | Comments:

Hello world

1
2
3
4
5
6
package test.ant;
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello world1");
}
};

假设我们要将上的代码编译,移动,打包,允许。最直接的方法是手动用java, cp, jar, java完成。如果需要测试操作还需要对测试代码进行同样的操作。

当然这里的情况是很简单的,但是考虑到如果有成百上千个类,包,子项目,存在复杂的相互依赖,外部依赖等情况,同样多的测试类,以及复杂的部署过程。而且这样的过程在开发过程可能需要反复进行很多次。

手动完成几乎是不可能的任务,而且非常慢和不可靠。
通过Ant我们可以简洁优雅的完成这些复杂的任务,只需要一个或几个命令。

Ant的配置这里就不在细述,可以参考官网,需要注意的是在配置Ant时需要包装java环境已经配置好。

Ant是一个Apache基金会下的跨平台的基于Java语言开发的构件工具。
其构建文件默认为build.xml,默认放在项目的根目录下。

对于上面的HelloWorld任务,我们可以编写build.xml如下:

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
<?xml version="1.0" encoding="UTF-8" ?>
<project name="HelloWorld" default="run" basedir=".">
<property name="src" value="src"/>
<property name="dest" value="classes"/>
<property name="hello_jar" value="hello1.jar"/>

<target name="init">
<mkdir dir="${dest}"/>
</target>

<target name="compile" depends="init">
<javac srcdir="${src}" destdir="${dest}"/>
</target>

<target name="build" depends="compile">
<jar jarfile="${hello_jar}" basedir="${dest}"/>
</target>

<target name="run" depends="build">
<java classname="test.ant.HelloWorld" classpath="${hello_jar}"/>
</target>

<target name="clean">
<delete dir="${dest}" />
<delete file="${hello_jar}" />
</target>

<target name="rerun" depends="clean,run">
<ant target="clean" />
<ant target="run" />
</target>

</project>
1
2
<project name="HelloWorld" default="run" basedir=".">
# 项目名字以及一些属性配置,例如项目目录,默认的target
1
2
3
<property name="src" value="src"/>
<property name="dest" value="classes"/>
<property name="hello_jar" value="hello1.jar"/>

属性任务(相当于变量),一般会用于后面的target中。

Ant的构建过程是由一个一个的target组成的,每个target有其名字和行为,以及所依赖的target。

属性 描述
目标名 (name) 表示目标的名称。(必须)
依赖 (depends) 用于描述 target 之间的依赖关系,若与多个 target 存在依赖关系时,需要以“,”间隔。Ant 会依照 depends 属性中 target 出现的顺序依次执行每个 target。被依赖的 target 会先执行。(可选)
描述 (description) 关于 target 功能的简单描述。(可选)
如果 (if) 用于验证指定的属性是否存在,若不存在,所在 target 将不会被执行。(可选)
除非 (unless) 该属性的功能与 if 属性的功能正好相反,它也用于验证指定的属性是否存在,若不存在,所在 target 将会被执行。(可选)
1
2
3
4
5
6
7
<target name="init">
<mkdir dir="${dest}"/>
</target>

<target name="compile" depends="init">
<javac srcdir="${src}" destdir="${dest}"/>
</target>

如果一个target有所依赖的target,则在执行这个target之前,会先需要完成所依赖的target的执行。也就是,target之间有一个依赖关系,这种关系用depends来指定。即如果Target A依赖于Target B,那么在执行Target A之前会首先执行Target B。

完成build.xml后(复杂的项目时一般不需要自己些,ant提供了一系列工具自动生成build.xml),我们只需要通过下面命令

1
ant

就可以完成项目的编译,打包,运行,只输入ant时,会执行到默认的target,默认target所依赖(递归)的target也会被全部执行。

如果只希望执行到某个阶段,只需要打包,可以使用ant target-name。

例如如果只需要打包,可以使用:

1
ant build

More

子项目

一个比较复杂的项目通常包括多个子项目,并且各个子项目是由不同的开发人员来完成的。在这种情况下,不同的子项目应该拥有不同的build.xml,整个项目有一个汇总build.xml,通过Ant任务或是AntCall任务调用子项目的build.xml,如下例:

1
2
3
4
5
<target name="core" depends="init">
<ant dir="components" target="core"/>
<ant dir="waf/src" target="core"/>
<ant dir="apps" target="core"/>
</target>

在各个子项目的耦合不是非常紧密的情况下,各个子项目应该有各自独立的目录结构,也就是说它们可以有自己的src、doc、build、dist等目录及自己的build.xml文件,但是可以共享lib和bin目录。而对于那些耦合紧密的子项目,则推荐使用同一个src目录,但是不同的子项目有不同的子目录,各个子项目的build.xml文件可以放在根目录下,也可以移到各个子项目的目录下。

属性文件

当你只需要对小部分属性进行设置时,可以选择直接在构建文件中设置。然而,对于大项目,最好将设置属性的信息存储在一个独立的文件中。使用外部的Property文件可以保存一些预设置的公共属性变量。这些属性可以在多个不同的Build文件中使用。

存储属性信息在一个独立的文件中将会提供以下好处:

  • 它可以让您重复使用相同的构建文件,该文件在不同的执行环境中使用不同的属性设置。例如,构建属性文件在 DEV , TEST , 和 PROD 环境中可以独立地被维护。
  • 当你事先不知道属性的值时(例如,在一个实际的环境中),这样处理是有益的。这样允许你在知道属性值后,在其他环境中执行生成 (build) 操作。

一般情况下,属性文件都被命名为build.properties, 并且与 build.xml 存放在同一目录层。 你可以基于部署环境,比如: build.properties.dev 和 build.properties.test 创建多个 build.properties文件。

在下面的例子中展示了 build.xml 文件和与之相联系的build.properties文件:

build.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<project name="Hello World Project" default="info">

<property file="build.properties"/>

<target name="info">
<echo>Apache Ant version is ${ant.version} - You are at ${sitename} </echo>
</target>

</project>

build.properties

1
2
3
# The Site Name
sitename=wiki.jikexueyuan.com
buildversion=3.3.2

注意到上面的练习中,sitename 是一个自定义属性,执行后映射到一个地址为 “wiki.jikexueyuan.com” 的网站上。你可以用这种方式声明任意数量的属性。在上面的例子中,还有一个自定义属性 buildversioin,它表明了当前构建的版本号。

外部XML文件导入

可以将一个外部的XML文件导入Build文件中,这样多个项目的开发者可以通过引用来共享一些代码,同样,这也有助于Build文件的重用,示例代码如下所示:

1
2
3
4
5
6
7
8
<!DOCTYPE project [
<!ENTITY share-variable SYSTEM "file:../share-variable.xml">
<!ENTITY build-share SYSTEM "file:../build-share.xml">
]>
<project name="main" default="complie" basedir=".">
&share-variable;
&build-share;
... ...

数据类型

Ant 提供一些预定义的数据类型。不要将术语“数据类型”和那些在编程语言中可用的数据类型相混淆,而是将他们视作一组已经在产品中配置好的服务。

下述的数据类型是由 Apache Ant 提供的。

文件集

文件集的数据类型代表了一个文件集合。它被当作一个过滤器,用来包括或移除匹配某种模式的文件。

例如,参考下面的代码。这里,src 属性指向项目的源文件夹。

文件集选择源文件夹中所有的 .java 文件,除了那些包含有 ‘Stub’ 单词的文件。能区分大小写的过滤器被应用到文件集上,这意味着名为 Samplestub.java 的文件将不会被排除在文件集之外。

1
2
3
4
<fileset dir="${src}" casesensitive="yes">
<include name="**/*.java"/>
<exclude name="**/*Stub*"/>
</fileset>

模式集合

一个模式集合指的是一种模式,基于这种模式,能够很容易地过滤文件或者文件夹。模式可以使用下述的元字符进行创建。

  • ? -仅匹配一个字符
    • -匹配零个或者多个字符
  • ** -递归地匹配零个或者多个目录

下面的例子演示了模式集合的使用。

1
2
3
4
<patternset id="java.files.without.stubs">
<include name="src/**/*.java"/>
<exclude name="src/**/*Stub*"/>
</patternset>

该模式集合能够通过一个类似于下述的文件集进行重用:

1
2
3
<fileset dir="${src}" casesensitive="yes">
<patternset refid="java.files.without.stubs"/>
</fileset>

文件列表

文件列表数据类型与文件集相类似,除了以下几处不同:

文件列表包含明确命名的文件的列表,同时其不支持通配符。
文件列表数据类型能够被应用于现有的或者还不存在的文件中。
让我们来看一个下述的关于文件列表数据类型的例子。在这个例子中,属性 webapp.src.folder 指向该项目中的 Web 应用的源文件夹。

1
2
3
4
5
6
<filelist id="config.files" dir="${webapp.src.folder}">
<file name="applicationConfig.xml"/>
<file name="faces-config.xml"/>
<file name="web.xml"/>
<file name="portlet.xml"/>
</filelist>

过滤器集合

使用一个过滤器集合数据类型与拷贝任务,你可以在所有文件中使用一个替换值来替换掉一些与模式相匹配的文本。

一个常见的例子就是对一个已经发行的说明文件追加版本号,代码如下:

1
2
3
4
5
6
<copy todir="${output.dir}">
<fileset dir="${releasenotes.dir}" includes="**/*.txt"/>
<filterset>
<filter token="VERSION" value="${current.version}"/>
</filterset>
</copy>

在这段代码中:

  • 属性 output.dir 指向项目的输出文件夹。
  • 属性 releasenotes.dir 指向项目的发行说明文件夹。
  • 属性 current.version 指向项目的当前版本文件夹。
  • 拷贝任务,顾名思义,是用来将文件从一个地址拷贝到另一个地址。

路径

path数据类型通常被用来表示一个类路径。各个路径之间用分号或者冒号隔开。然而,这些字符在运行时被替代为执行系统的路径分隔符。

类路径被设置为项目中 jar 文件和类文件的列表,如下面例子所示:

1
2
3
4
5
6
<path id="build.classpath.jar">
<pathelement path="${env.J2EE_HOME}/${j2ee.jar}"/>
<fileset dir="lib">
<include name="**/*.jar"/>
</fileset>
</path>

在这段代码中:

  • 属性 env.J2EE_HOME 指向环境变量 J2EE_HOME 。
  • 属性 j2ee.jar 指向在 J2EE 基础文件夹下面的名为 J2EE jar 的文件。

Example project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C:\work\FaxWebApplication>tree
Folder PATH listing
Volume serial number is 00740061 EC1C:ADB1
C:.
+---db
+---src
. +---faxapp
. +---dao
. +---entity
. +---util
. +---web
+---war
+---images
+---js
+---META-INF
+---styles
+---WEB-INF
+---classes
+---jsp
+---lib

下面给出上述项目的 build.xml 文件的内容。让我们来一条语句接一条语句地来分析它。

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
<?xml version="1.0"?>
<project name="fax" basedir="." default="build">
<property name="src.dir" value="src"/>
<property name="web.dir" value="war"/>
<property name="build.dir" value="${web.dir}/WEB-INF/classes"/>
<property name="name" value="fax"/>

<path id="master-classpath">
<fileset dir="${web.dir}/WEB-INF/lib">
<include name="*.jar"/>
</fileset>
<pathelement path="${build.dir}"/>
</path>

<target name="build" description="Compile source tree java files">
<mkdir dir="${build.dir}"/>
<javac destdir="${build.dir}" source="1.5" target="1.5">
<src path="${src.dir}"/>
<classpath refid="master-classpath"/>
</javac>
</target>

<target name="clean" description="Clean output directories">
<delete>
<fileset dir="${build.dir}">
<include name="**/*.class"/>
</fileset>
</delete>
</target>

<target name="build-jar">
<jar destfile="${web.dir}/lib/util.jar"
basedir="${build.dir}/classes"
includes="faxapp/util/**"
excludes="**/Test.class">

<manifest>
<attribute name="Main-Class" value="com.tutorialspoint.util.FaxUtil"/>
</manifest>

</jar>
</target>
</project>

首先,让我们来声明一些源文件,web 文件和构建文件的一些属性信息。

1
2
3
<property name="src.dir" value="src"/>
<property name="web.dir" value="war"/>
<property name="build.dir" value="${web.dir}/WEB-INF/classes"/>

在上面的例子中:

src.dir 表示这个项目的源文件目录,也就是存储 java 文件的地方。
web.dir 表示这个项目的 web 文件目录,也就是存储 JSPs 文件,web.xml,css,javascript 以及其它与 web 相关的文件的地方。
build.dir 表示该项目的输出文件。
属性也可以引用其它属性。在上面的例子中,build.dir 属性引用了 web.dir 属性。

在上面的例子中,src.dir 就是项目源文件存放的地方。

我们项目的默认目标是编译目标。但是首先让我们来看一下 clean 目标。

clean 目标,就像它的名字所表明的意思一样,删除构建文件夹中的所有文件。

1
2
3
4
5
6
7
<target name="clean" description="Clean output directories">
<delete>
<fileset dir="${build.dir}">
<include name="**/*.class"/>
</fileset>
</delete>
</target>

控制类路径 (master-classpath) 保存类路径的相关信息。在这种情况下,它包含了构建文件夹和 jar 文件夹中的所有的类文件。

1
2
3
4
5
6
<path id="master-classpath">
<fileset dir="${web.dir}/WEB-INF/lib">
<include name="*.jar"/>
</fileset>
<pathelement path="${build.dir}"/>
</path>

最后,构建目标构建这些文件。首先,我们创建一个构建目录,如果该目录不存在,我们就执行 javac 命令(具体以 jdk 1.5 作为我们目标的编译环境)。 我们对 javac 任务提供源文件夹和类路径,并且通过执行 javac 任务将类文件存放在构建文件夹中。

1
2
3
4
5
6
7
8
<target name="build" description="Compile main source tree java files">
<mkdir dir="${build.dir}"/>
<javac destdir="${build.dir}" source="1.5" target="1.5" debug="true"
deprecation="false" optimize="false" failonerror="true">
<src path="${src.dir}"/>
<classpath refid="master-classpath"/>
</javac>
</target>

在这个文件上执行 Ant,编译 java 源文件,并将编译后的类文件存放在构建文件夹的地方。

运行 Ant 文件后,能看到以下输出:

1
2
BUILD SUCCESSFUL
Total time: 6.3 seconds

文件被编译后,将存储在 build.dir 文件夹中。

JUnit集成

参考Apache Ant教程, Ant教程

Java IO

Posted on 2014-07-05 | In java | Comments:

Java输入输出

File类

File能新建,删除,重命名文件和目录,但是File不能访问文件内容本身,如果需要访问文件内容本身,需要使用输入输出流。

访问文件和目录

File类可以使用文件路径字符串创建File实例,该文件路径可以是绝对路径,也可以是相对路径。

一旦创建了File对象后,就可以调用File对象的方法来访问。

  1. 访问文件名相关的方法

    • String getName()
    • String getPath()
    • File getAbsoluteFile()
    • String getAbsolutePath()
    • String getParent()
    • boolean renameTo(File newName)
  2. 文件检测相关方法

    • boolean exists()
    • boolean canWrite()
    • boolean canRead()
    • boolean isFile()
    • boolean isDirectory()
    • boolean isAbsolute()
  3. 获取常规文件信息

    • long lastModified()
    • long length()
  4. 文件操作相关方法

    • boolean createNewFile()
    • boolean delete()
    • void deletOnExit()
  5. 目录操作相关方法

    • boolean mkdir()
    • String[] list()
    • File[] listFiles()
    • static File[] listRoots()
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
public class FileTest
{
public static void main(String[] args)
throws IOException
{
// 以当前路径来创建一个File对象
File file = new File(".");
// 直接获取文件名,输出一点
System.out.println(file.getName());
// 获取相对路径的父路径可能出错,下面代码输出null
System.out.println(file.getParent());
// 获取绝对路径
System.out.println(file.getAbsoluteFile());
// 获取上一级路径
System.out.println(file.getAbsoluteFile().getParent());
// 在当前路径下创建一个临时文件
File tmpFile = File.createTempFile("aaa", ".txt", file);
// 指定当JVM退出时删除该文件
tmpFile.deleteOnExit();
// 以系统当前时间作为新文件名来创建新文件
File newFile = new File(System.currentTimeMillis() + "");
System.out.println("newFile对象是否存在:" + newFile.exists());
// 以指定newFile对象来创建一个文件
newFile.createNewFile();
// 以newFile对象来创建一个目录,因为newFile已经存在,
// 所以下面方法返回false,即无法创建该目录
newFile.mkdir();
// 使用list()方法来列出当前路径下的所有文件和路径
String[] fileList = file.list();
System.out.println("====当前路径下所有文件和路径如下====");
for (String fileName : fileList)
{
System.out.println(fileName);
}
// listRoots()静态方法列出所有的磁盘根路径。
File[] roots = File.listRoots();
System.out.println("====系统所有根路径如下====");
for (File root : roots)
{
System.out.println(root);
}
}
}

文件过滤器

File类的list()方法可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFileter接口包含一个accept(File dir, String name)方法,该放阿飞将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。

理解Java的IO流

在Java中把不同的输入输出源(键盘,文件,网络连接等)抽象为流,通过流的方式允许Java使用相同的方式来访问不同输入输出源。

流的分类

  1. 输入流和输出流
    输入流主要由InputStream和Reader作为基类,而输出流则主要有OutputStream和Writer作为基类。它们都是一些抽象基类无法直接创建实例。

  2. 字节流和字符流
    字节流和字符流的用法几乎完全一样,区别在于操作的数据单元不同,字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16为的字符。

字节流主要有InputStream和OutputStream作为基类,而字符流主要有Reader和Writer作为基类。

  1. 节点流和处理流(装饰器设计模式)

通过使用处理流来包装不同的节点流,即可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出功能,因此处理流也被称为包装流。

流的概念模型

  • InputStream/Reader: 所有输入流的基类
  • OutputStream/Writer:所有输出流的基类

水管,输入流—>从水管中取水滴
输出流—> 将水滴放入水管

处理流的功能:

  • 性能的提高:主要以增加缓冲的方式提高输入/输出效率
  • 便捷的操作:提供一系列方法依次输入/输出大批量的数据,而不是当个水滴。

字节流和字符流

InputStream和Reader

抽象基类,本身并不能创建实例来执行输入。

InputStream方法:

  • int read()
  • int read(byte[] b)
  • int read(byte[] b, int off, int len)

Reader方法:

  • int read()
  • int read(char[] cbuf)
  • int read(char[] cbuf, int off, int len)

用于读取文件的输入流: FileInputStream和FileReader,它们都是节点流,会直接和指定文件关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FileInputStreamTest
{
public static void main(String[] args) throws IOException
{
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileInputStreamTest.java");
// 创建一个长度为1024的“竹筒”
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
// 关闭文件输入流,放在finally块里更安全
fis.close();
}
}

OutputStream和Writer

OutputStream和Writer共有的方法:

  • void write(int c), c可以代表字节,也可以代表字符
  • void write(byte[]/char[] buf)
  • void write(bype[]/char[] buf, int off, int len)

Write另外的两个方法:

  • void write(String str)
  • void write(String str, int off, int len)

使用FileOutStream输出:

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
public class FileOutputStreamTest
{
public static void main(String[] args)
{
try(
// 创建字节输入流
FileInputStream fis = new FileInputStream(
"FileOutputStreamTest.java");
// 创建字节输出流
FileOutputStream fos = new FileOutputStream("newFile.txt"))
{
byte[] bbuf = new byte[32];
int hasRead = 0;
// 循环从输入流中取出数据
while ((hasRead = fis.read(bbuf)) > 0 )
{
// 每读取一次,即写入文件输出流,读了多少,就写多少。
fos.write(bbuf , 0 , hasRead);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

关闭输出流除了可以保证流的物理资源回收以外,还可以将输出缓冲区中的数据flush到屋里节点。

如果希望直接输入字符串内容,则使用FileWriter会有更好的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FileWriterTest
{
public static void main(String[] args)
{
try(
FileWriter fw = new FileWriter("poem.txt"))
{
fw.write("关闭输出流除了可以保证流的物理资源回收以外\n");
fw.write("还可以将输出缓冲区中的数据flush到屋里节点。\n"); }
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

输入/输出流体系

处理流的用法

处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法。

使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备,文件交互。

下面程序使用PrintStream处理流来包装OutputStream,使用处理流后输出将更加方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PrintStreamTest
{
public static void main(String[] args)
{
try(
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos))
{
// 使用PrintStream执行输出
ps.println("普通字符串");
// 直接使用PrintStream输出对象
ps.println(new PrintStreamTest());
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

PrintStream类的输出功能非常强大,如果需要输出文本内容,都应该将输出流包装成PrintStream后进行输出

关闭高层流时,系统会自动关闭被包装的节点流。

输入/输出流体系

分类 字节输入流 字节输出流 字节输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputSteam ByteArrayOutputSteam CharArrayReader CharArrayWriter
访问管道 PipedInputSteam PipedOutputStream PipedReader PipedWriter
访问字符串 - - StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 - - InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 - PrintStream - PrintWriter
推回输入流 PushbackInputSteam - PushbackReader
特殊流 DataInputStream DataOutputStream
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
public class StringNodeTest
{
public static void main(String[] args)
{
String src = "从明天起,做一个幸福的人\n"
+ "喂马,劈柴,周游世界\n"
+ "从明天起,关心粮食和蔬菜\n"
+ "我有一所房子,面朝大海,春暖花开\n"
+ "从明天起,和每一个亲人通信\n"
+ "告诉他们我的幸福\n";
char[] buffer = new char[32];
int hasRead = 0;
try(
StringReader sr = new StringReader(src))
{
// 采用循环读取的访问读取字符串
while((hasRead = sr.read(buffer)) > 0)
{
System.out.print(new String(buffer ,0 , hasRead));
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
try(
// 创建StringWriter时,实际上以一个StringBuffer作为输出节点
// 下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter())
{
// 调用StringWriter的方法执行输出
sw.write("有一个美丽的新世界,\n");
sw.write("她在远方等我,\n");
sw.write("哪里有天真的孩子,\n");
sw.write("还有姑娘的酒窝\n");
System.out.println("----下面是sw的字符串节点里的内容----");
// 使用toString()方法返回StringWriter的字符串节点的内容
System.out.println(sw.toString());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}

转换流

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
public class KeyinTest
{
public static void main(String[] args)
{
try(
// 将Sytem.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
//将普通Reader包装成BufferedReader
BufferedReader br = new BufferedReader(reader))
{
String buffer = null;
//采用循环方式来一行一行的读取
while ((buffer = br.readLine()) != null)
{
//如果读取的字符串为"exit",程序退出
if (buffer.equals("exit"))
{
System.exit(1);
}
//打印读取的内容
System.out.println("输入内容为:" + buffer);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

由于Buffered具有一个readLine()方法,可以非常方便地一次读入一行内容,所有经常包读取文件内容的输入包装为BufferedReader,用来方便地读取输入流的文本内容

推回输入流

PushbackInputStream和PushbackReader

  • void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到缓冲区里,从而允许重复度去刚刚读取的数据
  • void unread(byte[]/char[] buf, int off, int len)
  • void unread(int b)

推回的数据存储在推回缓冲区里,read()方法先会从推回缓冲区里读,没有数据才从原输入流中读取。

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
public class PushbackTest
{
public static void main(String[] args)
{
try(
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"PushbackTest.java") , 64))
{
char[] buf = new char[32];
// 用以保存上次读取的字符串内容
String lastContent = "";
int hasRead = 0;
// 循环读取文件内容
while ((hasRead = pr.read(buf)) > 0)
{
// 将读取的内容转换成字符串
String content = new String(buf , 0 , hasRead);
int targetIndex = 0;
// 将上次读取的字符串和本次读取的字符串拼起来,
// 查看是否包含目标字符串, 如果包含目标字符串
if ((targetIndex = (lastContent + content)
.indexOf("new PushbackReader")) > 0)
{
// 将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
// 指定读取前面len个字符
int len = targetIndex > 32 ? 32 : targetIndex;
// 再次读取指定长度的内容(就是目标字符串之前的内容)
pr.read(buf , 0 , len);
// 打印读取的内容
System.out.print(new String(buf , 0 ,len));
System.exit(0);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}

重定向标准输入/输出

Java的标准输入输出分别通过System.in和System.out来代表,默认情况分别是键盘和显示器。但可以通过下面方法重定向标准输入输出。

  • static void setErr(PrintStream err),重定向标准错误输出流
  • static void setIn(InputStream in),重定向标准输入流
  • static void setOut(PrintStream out),重定向标准输出流
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
public class RedirectOut
{
public static void main(String[] args)
{
try(
// 一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt")))
{
// 将标准输出重定向到ps输出流
System.setOut(ps);
// 向标准输出输出一个字符串
System.out.println("普通字符串");
// 向标准输出输出一个对象
System.out.println(new RedirectOut());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
```

```java
public class RedirectIn
{
public static void main(String[] args)
{
try(
FileInputStream fis = new FileInputStream("RedirectIn.java"))
{
// 将标准输入重定向到fis输入流
System.setIn(fis);
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}

Java虚拟机读取其他进程的数据

由Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,代表有该Java程序启动的子进程。Process类提供了如下三个方法,用于让程序和其子进程进行通信。

  • InputStream getRerrorStream()
  • InputSteam getInputStream()
  • OutputStream getOutputStream()

如果要让子进程读取程序中的数据,应该是输出流,而不是输入流。要站在主java程序的角度看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReadFromProcess
{
public static void main(String[] args)
throws IOException
{
// 运行javac命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");
try(
// 以p进程的错误流创建BufferedReader对象
// 这个错误流对本程序是输入流,对p进程则是输出流
BufferedReader br = new BufferedReader(new
InputStreamReader(p.getErrorStream())))
{
String buff = null;
// 采取循环方式来读取p进程的错误输出
while((buff = br.readLine()) != null)
{
System.out.println(buff);
}
}
}
}
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
public class WriteToProcess
{
public static void main(String[] args)
throws IOException
{
// 运行java ReadStandard命令,返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("java ReadStandard");
try(
// 以p进程的输出流创建PrintStream对象
// 这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream()))
{
// 向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("普通字符串");
ps.println(new WriteToProcess());
}
}
}
// 定义一个ReadStandard类,该类可以接受标准输入,
// 并将标准输入写入out.txt文件。
class ReadStandard
{
public static void main(String[] args)
{
try(
// 使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(
new FileOutputStream("out.txt")))
{
// 增加下面一行将只把回车作为分隔符
sc.useDelimiter("\n");
// 判断是否还有下一个输入项
while(sc.hasNext())
{
// 输出输入项
ps.println("键盘输入的内容是:" + sc.next());
}
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}

RandomAccessFile

RandomAccessFile是功能最丰富的文件内容访问类,但是只能读写文件,不能读写其他IO节点。

与普通输入输出流不同的是,RandomAccessFile支持随机访问的方式,程序可以直接跳转到文件的任何位置读写数据。如果只需要访问文件部分内容,使用RandomAccessFile将是更好的选择。

RandomAccessFile允许自由定位文件记录指针,以及相关操作。

  • long getFilePointer
  • void seek(long pos),移动文件记录指针到pos位置

RandomAccessFile同时包含InputStream的三个read()方法,以及OutputStream的三个write()方法

Random这里指的是任意访问,而不是随机访问。四个访问模式:

  • r
  • rw
  • rws
  • rwd
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
public class RandomAccessFileTest
{
public static void main(String[] args)
{
try(
RandomAccessFile raf = new RandomAccessFile(
"RandomAccessFileTest.java" , "r"))
{
// 获取RandomAccessFile对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:"
+ raf.getFilePointer());
// 移动raf的文件记录指针的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环来重复“取水”过程
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
System.out.print(new String(bbuf , 0 , hasRead ));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AppendContent
{
public static void main(String[] args)
{
try(
//以读、写方式打开一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
{
//将记录指针移动到out.txt文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
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
public class InsertContent
{
public static void insert(String fileName , long pos
, String insertContent) throws IOException
{
File tmp = File.createTempFile("tmp" , null);
tmp.deleteOnExit();
try(
RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
// 创建一个临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp))
{
raf.seek(pos);
// ------下面代码将插入点后的内容读入临时文件中保存------
byte[] bbuf = new byte[64];
// 用于保存实际读取的字节数
int hasRead = 0;
// 使用循环方式读取插入点后的数据
while ((hasRead = raf.read(bbuf)) > 0 )
{
// 将读取的数据写入临时文件
tmpOut.write(bbuf , 0 , hasRead);
}
// ----------下面代码插入内容----------
// 把文件记录指针重新定位到pos位置
raf.seek(pos);
// 追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容
while ((hasRead = tmpIn.read(bbuf)) > 0 )
{
raf.write(bbuf , 0 , hasRead);
}
}
}
public static void main(String[] args)
throws IOException
{
insert("InsertContent.java" , 45 , "插入的内容\r\n");
}
}

对象序列化

序列化的含义和意义

对象序列话的目标是将对象保存到磁盘中,或者允许在网络中直接传输对象。对象序列化机制允许把内容的Java对象转换成平台无关的二进制流,程序也可以通过反序列化恢复该Java对象。

如果需要让某个对象支持序列化,必须实现如下两个借口之一:

  • Serializable
  • Externalizable

Java很多类已经实现Serializable,该借口是一个标记借口,实现该接口无需实现任何方法,只是表明是可序列化的。

所有可能在网络上传输或需要存储到磁盘的的类都应该是可以序列化的,否则会出现异常。

使用对象流实现序列化

ObjectOutputStream,处理流,必须建立在其他节点流的基础上。

1
2
3
ObjectOutputSream oos = new ObjectOutputSteam(new FileOutputStream("test.txt"))

oos.writeObject(per);
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
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

}

ObjectInputSteam,处理流,需要建立在其他节点流基础上。

1
2
3
ObjectInputStream ois = new ObjectInputStream(new FileInputSteam("test.txt"))

Person p = (Person) ois.readObject();

反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供Java对象的类class文件。另外,反序列化无需通过构造器来初始化Java对象。

如果使用序列化机制向文件写入多个java对象,使用反序列化机制恢复对象时必须按照实际写入的顺序读取。

当一个可序列化类有多个父类时(直接和间接),这些父类要么有无参数的构造器,要么也是可序列化的。如果父类是不可序列化的,只是带有无参构造器,则父类中定义的成员变量值不会序列化到二进制流中。

对象引用的序列化

如果某个类的成员变量是另一个类的对象的引用,也就是引用类型,那么这个引用类必须是可序列化,否则拥有该类对象的类也是不可序列化的。

当对某个对象序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果被引用的实例变量也引用了另一个对象,则该对象也会被序列化。这称为递归序列化。

如果多个Student引用同一个Class对象,这时在恢复时,这个Class对象应该只存在一份,而不是独立的两份。为了实现这点,Java序列化采用了一种特殊的序列化算法:

  • 所有保存磁盘中的对象都有一个序列化编号
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未在本次虚拟机中被序列化过,系统才会将该对象转换成字节序列输出。
  • 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是重新序列化该对象。

这意味着某个对象被序列化后,改变其状态(成员变量值),再次序列化,其改变后的状态不会在被序列化到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class WriteTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("teacher.txt")))
{
Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐僧" , per);
Teacher t2 = new Teacher("菩提祖师" , per);
// 依次将四个对象写入输出流
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
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
public class ReadTeacher
{
public static void main(String[] args)
{
try(
// 创建一个ObjectInputStream输出流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("teacher.txt")))
{
// 依次读取ObjectInputStream输入流中的四个对象
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
// 输出true
System.out.println("t1的student引用和p是否相同:"
+ (t1.getStudent() == p));
// 输出true
System.out.println("t2的student引用和p是否相同:"
+ (t2.getStudent() == p));
// 输出true
System.out.println("t2和t3是否是同一个对象:"
+ (t2 == t3));
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
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
public class SerializeMutable
{
public static void main(String[] args)
{

try(
// 创建一个ObjectOutputStream输入流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("mutable.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("mutable.txt")))
{
Person per = new Person("孙悟空", 500);
// 系统会per对象转换字节序列并输出
oos.writeObject(per);
// 改变per对象的name Field
per.setName("猪八戒");
// 系统只是输出序列化编号,所以改变后的name不会被序列化
oos.writeObject(per);
Person p1 = (Person)ois.readObject(); //①
Person p2 = (Person)ois.readObject(); //②
// 下面输出true,即反序列化后p1等于p2
System.out.println(p1 == p2);
// 下面依然看到输出"孙悟空",即改变后的Field没有被序列化
System.out.println(p2.getName());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

自定义序列化

通过在实例变量前使用transient关键字修饰,可以制定Java序列化时无需理会该实例变量。只能用来修饰实例变量。

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
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}

在序列化和反序列化需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法可以用来实现自定义序列化:

  • private void writeObject(ObjectOutputStream)
  • private void readObject(ObjectInputStream in)
  • private void readObjectNoData()
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
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
// 将name Field的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name Field
this.name = ((StringBuffer)in.readObject()).reverse()
.toString();
this.age = in.readInt();
}
}

writeOject()方法存储实例变量的顺序应该和readOject()方法中恢复实例变量的顺序一致,否则将不能正常恢复该java对象。

更彻底的自定义机制,甚至可以在序列化对象时将对象替换为其他对象:
Object writeReplace()

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
public class Person
implements java.io.Serializable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

// 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
private Object writeReplace()throws ObjectStreamException
{
ArrayList<Object> list = new ArrayList<>();
list.add(name);
list.add(age);
return list;
}
}
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
public class ReplaceTest
{
public static void main(String[] args)
{
try(
// 创建一个ObjectOutputStream输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("replace.txt"));
// 创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("replace.txt")))
{
Person per = new Person("孙悟空", 500);
// 系统将per对象转换字节序列并输出
oos.writeObject(per);
// 反序列化读取得到的是ArrayList
ArrayList list = (ArrayList)ois.readObject();
System.out.println(list);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

相对应的另一个可以实现保护性复制整个对象:
Object readResolve()

另一种自定义个序列化机制

Java类实现Externalizable接口,完全由程序员决定存储和恢复对象数据。

  • readExternal(ObjectInput in)
  • void writeExternal(ObjectOutput out)

实际上采用Externalizable接口方式实现的序列化与前面的介绍的自定义序列化非常相似,只是这里是强制的。

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
public class Person
implements java.io.Externalizable
{
private String name;
private int age;
// 注意此处没有提供无参数的构造器!
public Person(String name , int age)
{
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
// 省略name与age的setter和getter方法

// name的setter和getter方法
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}

// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}

public void writeExternal(java.io.ObjectOutput out)
throws IOException
{
// 将name Field的值反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in)
throws IOException, ClassNotFoundException
{
// 将读取的字符串反转后赋给name Field
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}

另外要注意的几点

  • 对象的实例变量(包括基本类型,数组,其他对象的引用)都会被序列化,方法,类变量,transitent遍历不会被序列化。
  • 反序列化必须有序列化对象的class文件
  • 当通过文件网络来读取序列化后的对象时,必须按照实际的写入的顺序读取。
  • Java的class文件的版本,可以通过为序列化类提供一个private static final的serialVersionUID值,用于表示该Java类的序列化版本,也就是说一个类升级后,只要它的serialVersionUI值不变,序列化机制也会把它们当做同一个序列化版本。

NIO

前面的面向流的IO都是阻塞式的,且都是通过字节移动来处理(即使不直接处理字节流,其底层的实现还是依赖字节处理),也就是面向流的输入输出系统一次只能处理一个字节,通常效率不高。

JDK1.4开始Java提供了一系列的改进的输入输出处理的新功能,统称为NIO。

Java NIO概述

NIO和传统IO目的相同,都是用于输入输出,但NIO采用了不同的方式来处理输入输出,NIO采用内存映射文件的方式来处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了,效率比传统IO快很多。

Channel和Buffer是NIO中的两个核心对象,Channel是对传统IO的模拟,NIO所有数据都需要通过Channel传输。Channel与传统IO Stream最大的区别是提供了一个map()方法,通过该方法可以直接将一块数据映射到内存中。如果说传统的输入输出是面向流的处理,则NIO则是面向块的处理。

Buffer可以理解为一个容器,本质上是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

除了Channel和Buffer之外,NIO还提供了用于将Unicode字符映射成字节系列以及逆映射操作的Charset类,也提供了用于支持非阻塞式IO的Selector类。

使用Buffer

从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层自己数组上进行get/set操作。另外对应基本类型都有相应的Buffer类(除boolean):CharBuffer,ShortBuffer,IntBuffer…

除ByteBuffer以外,都采用相同或相似的方法来管理数据。Buffer类都没有提供构造器,使用下面方法来得到Buffer对象:

static XxxBuffer allocate(int capacity)

用的比较多的是ByteBuffer和CharBuffer。ByteBuffer还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部映射到内存中后得到的结果,通常MappedByteBuffer对象有Channel的map()方法返回。

重要概念:

  • capacity:该Buffer的最大数据容量,创建后不能改变
  • limit:第一个不应该被读出或写入的缓冲区的位置。也就是说,limit后的数据既不可被读,也不可被写。
  • position:下一个可被读出或写入的位置,也就是前面是已经读写的区域。
  • mark:允许直接将position定位到mark处

0<=mark<=position<=limit<=capacity

Buffer的主要作用就是装入数据,然后输出数据。

开始时Buffer的position为0,limit为capacity,通过put()方法向Buffer放入一些数据,每放入一些数据,Buffer的position相应的向后移动一些位置。

当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0。也就是Buffer调用flip()方法之后,Buffer为输出数据做好准备,

当Buffer输出结束后,Buffer调用clear()方法,仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。

flip(), 为从Buffer取出数据做好准备
clear(), 为再次向Buffer中装入数据做好准备

其他方法:

  • int capacity()
  • boolean hasRemaining()
  • int limit()
  • Buffer limit()
  • Buffer mark()
  • int position()
  • Buffer position(int newPs)
  • int remaining()
  • Buffer reset():将position转到mark所在位置
  • Buffer rewind():将位置设置为0,取消设置的mark

另外还有put(), get()方法,既支持对单个数据的访问,也支持对批量数据的访问(以数组为参数),方式:

  • 相对,从当前position处开始,并移动position
  • 绝对,直接更具索引读写,不影响position值。
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
public class BufferTest
{
public static void main(String[] args)
{
// 创建Buffer
CharBuffer buff = CharBuffer.allocate(8); //①
System.out.println("capacity: " + buff.capacity());
System.out.println("limit: " + buff.limit());
System.out.println("position: " + buff.position());
// 放入元素
buff.put('a');
buff.put('b');
buff.put('c'); //②
System.out.println("加入三个元素后,position = "
+ buff.position());
// 调用flip()方法
buff.flip(); //③
System.out.println("执行flip()后,limit = " + buff.limit());
System.out.println("position = " + buff.position());
// 取出第一个元素
System.out.println("第一个元素(position=0):" + buff.get()); // ④
System.out.println("取出一个元素后,position = "
+ buff.position());
// 调用clear方法
buff.clear(); //⑤
System.out.println("执行clear()后,limit = " + buff.limit());
System.out.println("执行clear()后,position = "
+ buff.position());
System.out.println("执行clear()后,缓冲区内容并没有被清除:"
+ "第三个元素为:" + buff.get(2)); // ⑥
System.out.println("执行绝对读取后,position = "
+ buff.position());
}
}

通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来直接创建Buffer,成本更高,但读取效率更高,一般只用于长生存期的Buffer。另外只有ByteBuffer提供直接创建。

使用Channel

Channel类似与传统的流对象,但与传统的流对象有两个主要区别

  • Channel可以直接将指定文件的部分或全部映射成Buffer。
  • 程序不能直接访问Channel中的数据,包括读取写入都不行,Channel只能与Buffer进行交互。也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。

Java为Channel接口提供了DatagramChannel, FileChannel, Pipe.SinkChannel, Pipe.SourceChannel, SelectableChannel, ServerSocketChannel, SocketChannel等实现类。下面主要介绍FileChannel。

所有的Channel都不应该通过构造器直接创建,而是通过传统的节点流的getChannel()方法来返回对应的Channel。

最常用方法:

  • map(),将Channel对应的部分或全部数据映射为ByteBuffer
  • read()
  • write()

MappedByteBuffer map(FileChannel.MapMode mode, long position, long size), 模式分别有只读,读写等,第二第三个参数控制将Channel的哪些数据映射为ByteBuffer。

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
public class FileChannelTest
{
public static void main(String[] args)
{
File f = new File("FileChannelTest.java");
try(
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt")
.getChannel())
{
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel
.MapMode.READ_ONLY , 0 , f.length()); // ①
// 使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
// 直接将buffer里的数据全部输出
outChannel.write(buffer); // ②
// 再次调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RandomFileChannelTest
{
public static void main(String[] args)
throws IOException
{
File f = new File("a.txt");
try(
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel())
{
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel
.MapMode.READ_ONLY, 0 , f.length());
// 把Channel的记录指针移动到最后
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
}
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
public class ReadFile
{
public static void main(String[] args)
throws IOException
{
try(
// 创建文件输入流
FileInputStream fis = new FileInputStream("ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel())
{
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(64);
// 将FileChannel中数据放入ByteBuffer中
while( fcin.read(bbuff) != -1 )
{
// 锁定Buffer的空白区
bbuff.flip();
// 创建Charset对象
Charset charset = Charset.forName("GBK");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);
// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}

字符集和Charset

Java提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法。提供了一个avaiableCharsets()来获取当前JDK所支持的字符集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CharsetTest
{
public static void main(String[] args)
{
// 获取Java支持的全部字符集
SortedMap<String,Charset> map = Charset.availableCharsets();
for (String alias : map.keySet())
{
// 输出字符集的别名和对应的Charset对象
System.out.println(alias + "----->"
+ map.get(alias));
}
}
}

一旦直到了字符集的别名后,程序就可以调用Charset的forName方法来创建对应Charset对象:

1
2
Charset cs = Charset.forName("GBK");
Charset cscn = Charset.forName("ISO-8859-1");

然后就可以通过该对象的newDecoder(), newEncode()这两个方法分别返回CharsetDecoder和CharsetEncode对象,代表该Charset的解码器和编码器。

调用CharsetEncoder的encode方法就可以将CharBuffer或String转换成ByteBuffer。

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
public class CharsetTransform
{
public static void main(String[] args)
throws Exception
{
// 创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
// 获取cn对象对应的编码器和解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
// 创建一个CharBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put('孙');
cbuff.put('悟');
cbuff.put('空');
cbuff.flip();
// 将CharBuffer中的字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
// 循环访问ByteBuffer中的每个字节
for (int i = 0; i < bbuff.capacity() ; i++)
{
System.out.print(bbuff.get(i) + " ");
}
// 将ByteBuffer的数据解码成字符序列
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}

文件锁

Java 7的NIO.2

Path, Paths和Files核心API

使用FileVistor遍历文件和目录

使用WatchService监控文件变化

访问文件属性

Linux环境变量配置

Posted on 2014-07-02 | In Developing | Comments:

修改环境配置文件,主要有三种方法:

  • 修改/etc/profile,一般需要root权限。
  • 修改用户目录下的.bashrc文件,推荐使用。(或者bash_profile)
  • 直接在shell下修改,终端关闭后配置的内容失效。

如配置ANT的环境变量:

  • 进入用户主目录,vi .bashrc
  • 添加:
1
2
ANT_HOME=/home/chen/software/apache-ant-1.9.6
export PATH=$ANT_HOME/bin:$PATH
  • source .bashrc
  • which ant可以确认配置好的ant路径。

默认需要用户重新登录生效,通过source配置文件可以使得修改立即生效。
echo $PATH,查看配置后的路径。

更详细的设置可以参考MAC 设置环境变量path的几种方法.

1…14151617
Plusaber

Plusaber

Plusaber's Blog
82 posts
12 categories
22 tags
Links
  • LinkedIn
  • Indeed
  • Baito
  • Kaggle
© 2014 – 2019 Plusaber
Powered by Hexo v3.8.0
|
Theme – NexT.Mist v7.1.1