输入和输出流:
获取流对象
从文件中获取
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
从URL中获取
URL url = new URL("http://www.baidu.com");
InputStream in = url.openStream();
从字节数组中获取
byte[] bytes = ...;
InputStream in = new ByteArrayInputStream(bytes);
输出到字节数组
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] bytes = out.toByteArray();
获取字节
InputStream in = ...;
int b = in.read();//放回对应的整数,范围是0-255,也可能由于达到输入源的末尾返回-1.
值得注意的是,java的字节类型值得范围是-128~127,在你检查返回的值确定不是-1后,可以将返回值转换成字节类型。
byte[] bytes = ...;
actualBytesRead = in.read(bytes);//返回的是读取的字节的数量
actualBytesRead = in.read(bytes,start,length);
Java类库中没有一个方法能够直接从一个输入流中读取所有字节,但是可以自己构建,如下
public static byte[] readAllBytes(InputStream in) throws IOException{
ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(in,out);
out.close();//当完成写输出流的操作后,一定要关闭输出流确定提交了所有的缓存输出,最好使用try-with-catch语句
return out.toByteArray();
}
public static void copy(InputStream in, OutputStream out) throws IOException{
final int BLOCKSIZE = 1024;
byte[] bytes = new byte[BLOCKSIZE];
int len;
while ((len = in.read(bytes))!=-1)out.write(bytes,0,len);//当没有读取到结束标志时,一直循环输出
}
字符集编码
值得注意的是UTF-16编码存在大端字节序和小端字节序,会在文件头被注明,例如0xFEFF,根据这个值可以得到字节顺序,有的程序也会为UTF-8编码的文件也加入字节顺序标志,但实际上是不被需要的,所以当发现字节顺序标志时,要注意忽略掉。
ISO 8859-1编码,单字节编码,用于西欧语言使用的带重音的字符
Shift-JIS 可变长度编码日文的字符集
目前还没有一个可靠的方法检测一个输入流的字符集编码,有些API会使用默认字符集,即操作系统使用的字符集。在读取网页的时候,可以检查Content-type头的值,来判定字符集。
平台的编码字符集可以通过静态方法Charset.defaultCharset获得。
StandardCharsets类有Charset类型的静态变量时所有java虚拟机都支持的字符编码集:UTF-8,UTF-16,UTF-16BE,UTF-16LE,ISO_8859-1,US_ASCII
转换字节数组到字符串
String str = new String(bytes,Standard.UTF_8);
文本输入
使用Reader读取文本,可以使用InputStreamReader适配器从任何一种输入流获取一个Reader。
InputStream inStream = ...;
Reader in = new InputStreamReader(inStream,charset);
int ch = in.read();这个方法会返回一个范围在0~65535之间的编码单元,也会在到达输入源的末尾而返回-1.
对于比较短的文本文件,
String content = new String(Files.readAllBytes(path),charset);
如果按行读取
List
lines = Files.readAllLines(path,charset);
按流处理
try(Stream lines = Files.lines(path,charset)){
...
}
值得注意的是,按照流读取时,会出现IOException异常,则会被包装到UncheckedIOexception异常,抛到流操作之外
如果是从文件中读取数字或者单词时,可以用Scanner类,
Scanner in = new Scanner(path,"UTF-8");
while(in.hasNextDouble){
double value = in.nextDouble();
}
读取单词时,将scanner的分隔符设置为一个正则表达式,从而可以根据需要匹配和识别单词,
in.useDelimter("\\PL+");
如果输入源并非来自文件,则可以将InputStream再封装到BufferedReader;
try(BufferReader reader = new BufferReader(new InputStream(url.openStream()))){
...
}
直接从文件中得到Reader
Files.newBufferedReader(path,charset);
文本输出
使用Writer写文本
OutputStream outStream = ...;
Writer out = new OutputStreamWriter(outStream,charset);
out.writer(str);//输出流写到str中
获得一个文件的writer
Writer out = Files.newBufferedWriter(path,charset);
使用PrintWriter会更方便
PrintWriter out = new PrintWriter(Files.new BufferedWriter(path,charset));//写到文件
PrintWriter out = new PrintWriter(outStream,"UTF_8");//写到另一个输出流
写入文件
String content = ...;
Files.write(path,content.getBytes(charset));
或者
Files.write(path,lines,charset);//这里的lines可以是Cllection,或者Iterable extends CharSequence>;
追加内容到文件
Files.write(path,content.getBytes(charset),StandardOpenOption.APPEND);
读写二进制数据
DataInput接口可以用来读取数字、字符、布尔值、二进制格式的字符串,DataOutput中也声明了类似方法来写入这些类型。,注意这些都是大端字节序。
使用二进制输入和输出的好处是长度固定和效率高,例如writerInt总是会将一个整数写成一个四个字节big-endian的结果,无论这个整数是几位数,即每个值所占的空间相同,提高随机读取的效率
随机存取文件
RandomAccessFile类支持对一个文件进行随机的读写操作。
RandomAccessFile file = new RandomAccessFile(path.toString,"rw");
一个随机存取文件打开时会有一个文件指针指向下一个读或者写字节的位置,利用seek方法可以指定文件指针到一个指定的字节位置,通过getFilePointer得到当前指针位置
可以使用readInt和writerInt方法对一个随机存取文件进行数字的读写。
内存映射文件
FileChannel channel = FileChannel.open(path,StandardOpenOption.READ,standardOpenOption>WRITE);
ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITER,0,channel.size());
再用get、getInt、getdouble等方法读取数据,或者相应的使用put方法写数据
当通道关闭时,对映射内存的改变会写回到对应的文件。
文件锁
当多个同时执行的程序对同一个文件进行修改时,它们之间需要协调,否则文件很容易损坏,使用文件锁机制可以解决这类问题。
FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock();
或者FileLock lock = channel.tryLock();
如果文件已经被锁,则第一种会调用阻塞,直到锁被解除,第二种则会立刻返回文件锁,或者文件被锁了则返回null。
路径、文件和目录
Path对象就是一组目录名称的序列,后面可以跟文件名。
Path absolute = Paths.get("/","home","cay");//绝对路径
Path relative = Paths.get("myapp","conf","user.properties");//相对路径
静态方法Path.get可以接收一个或多个字符串参数,这些参数会被默认的文件系统路径分隔符合并在一起,合并成功则返回path对象,失败则返回InvalidPathException。
在程序主目录上找文件
Path workPath = homeDirectory.resolve("myapp/work");
在父目录下找文件
Path tempPath = workPath.resolveSibling("temp");
normalize方法可以去除冗余的.和..路径(或者任何文件系统认定是冗余的路径).
创建文件和目录
创建新目录 Files.createDirectory(path);
创建中间目录 Files.createDirectories(path);
创建一个空文件 Files.createFile(path);//存在则抛出异常
检查文件是否存在 Files.exists(path);
复制、移动、删除文件
Files.copy(fromPath,toPath);
Files.move(fromPath,toPath);
Files.copy(fromPath,toPath,StandardCopyOption.REPLACE_EXISTING,StandardCopyOption.COPY_ATTRIBUTES);//在目标已存在时,可以用次来覆盖目标文件和文件属性
Files.move(fromPath,toPath,StandardCopyOption.ATOMIC_MOVE);//指定为原子操作,即不会被多线程任务打断
Files.delete(path);//指定文件目录不存在会抛出异常
Files.deleteIfExists(path);
访问目录内容
Files.list返回一个服务目录项的Stream对象,其中目录读取是延迟的,目的是尽可能高效的处理含有大量目录项的目录,需要注意其不会进入子目录,如果需要处理一个目录的所有子目录,可以使用Files.walk方法。Files.walk方法按照深度优先次序访问。
每当遍历产生一个子目录时,他会先进入这个子目录,然后才会继续道和它平级的其他子目录或文件。
Files.walk(path,deepth);//可以限制访问的深度。
使用Files.walk方法来复制一个目录到另外一个目录。
Files.walk(source).forEach(p->{
try{
Path q = target.resolve(source.relativize(p));//从p到q的相对路径
if(Files.isDirectory(p)
Files.createDirectory(q);
else
Files.copy(p,q);
catch(IOExcetption ex){
throw new UncheckedIOexception(ex);
}
});
删除一个目录树的实例
Files.walkFileTree(root,new SimpleFileVistor(){
public FileVisitresult visisResult(Path file,BasicFileAttributes attrs) throws IOException{//遇到一个文件时
Files.delete(File);
return FilesVisitResult.CONTINUE;//继续访问下一个文件
}
public FileVisitresult postVisitDirectory(Path dir,IOException ex)throws IOException{//目录被处理之后
if(ex!=null) throw ex;
Files.delete(dir);
return FilesVisitResult.CONTINUE;//继续访问下一个文件
}
});
ZIP文件系统
FileSystem zipfs = FileSystem.newFileSystem(Path.get(zipname),null);//获得zip文件系统
Files.copy(zipfs.getPath(sourceName),targetPath);//从zip中复制文件到目标路径
Files.walk(zipfs.getPath("/").forEach(p->{//处理zip包中所有文件
处理p
});
创建一个新的zip文件包
Path zipPath = Paths.get("myfiles.zip");
URI uri = new URI("jar",zipPath.toUri().tostring(),null);//构造一个uri jar://myfile.zip
try(FileSystem zipfs = new FileSystem.newFileSystem(uri,Collections.singletonMap("create","true"))){
Files.copy(source,zipfs.getPath("/").resolve(targetPath));//复制文件到zip包中
}
URI连接
URLConnection connection = url.openConnection();//打开URL连接
connection.setRequestProperty("Accept-Charset","UTF-8,ISO-8859-1");//设置URLRequest属性值
connection.setDoOutput(true);//如果是post请求需要值为true,get请求则使默认的false
try (Writer out = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)){//打开写数据流
Map postdata = new HashMap<>();//设置要写的数据
postdata.put("aaa","bbb");
boolean first = true;
for(Map.Entry entry:postdata.entrySet()){
if(first)first = false;
else out.write("&");
out.write(URLEncoder.encode(entry.getKey(),"UTF-8"));//向服务器写数据
out.write("=");
out.write(URLEncoder.encode(entry.getValue(),"UTF-8"));
}
}
try(InputStream in = connection.getInputStream()){//对得到的数据进行分析
}
正则表达式
.
匹配任意单个字符 例如:.a.a 可以匹配java data
* 表示前面的表达式重复0次或者多次,+表示重复一次或者多次,?表示0次或者一次,例如 a+b?可以匹配a,ab,aab,aaab
| 表示可选 例如.(oo|ee)f 可以匹配beef或者woof
[] 表示可选字符的结合,例如[0-9],[A-Za-z]
^ 正则表达式开头,或者表示非
$ 正则表达式末尾
/a Alert ,/e 转义字符 ,/f 换页 ,/n 新一行 , /r 回车 ,/t tab
/d 数字 ,/w 词语符[a-zA-Z0-9] ,/s 空白字符[/n/r/t/f/x{B}]
用处
1.字符串是否匹配正则表达式
String regex = "[+-]?\\d+";//任意数字
CharSequence input = ...:
if(Pattern.matches(regex,input)){}//第一种
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if(matcher.matches())//第二种匹配方式,更为高效
匹配流元素
Stream strings = ...;
Stream result = strings.filter(pattern.asPredicate());
Matcher matcher = pattern.matcher(input);
while(matcher.find()){
String match = matcher.group();
...//可以处理每一个匹配结果
}
例子:
public static void patternString(){
String regex = ".a.a";//待匹配的模型
Pattern pattern = Pattern.compile(regex);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String next = scanner.next();
System.out.println("input: "+next);
Matcher matcher = pattern.matcher(next);
while (matcher.find()) {//匹配下一个,值得注意的是匹配规则不允许交错,即javava,仅能匹配java,不会匹配vava。
System.out.println("matcher: "+matcher.group());//处理匹配结果
}
}
}
分组(分组提取匹配结果)
例如匹配
BlackWell Toaster USD29.95
正则表达式写法: (\p{Alnum}+(\s+\p{Alnum}+)*)\s+([A-Z]{3})([0-9.]*)
注预定义关键字Alnum:匹配字母和数字,Lower匹配小写字母,Upper匹配大写字母,Alpha匹配Lower和Upper,Digit匹配数字,Punct匹配标点,Graph匹配打印字符,whitespace匹配空白字符(回车、制表、换行等),cntrl匹配Ascii码值小于32或者127的字符
分组匹配例子
Matcher matcher = pattern.matcher(input);
if(matcher.matches()){
item = matcher.group(1);//注意组是按照左括号的顺序排列的
currency = matcher.group(3);//从1开始,0是整个输入
price = matcher.group(4);
}
根据名称捕获
(?- \p{Alnum}+(\s+\p{Alnum}+)*)+\s+(?[A-Z]{3})(?[0-9.]*)
matcher.group("item");
消除和替换匹配结果
Pattern commas = Pattern.compile("\\s*,\\s*");
String[] tokens = commas.split(input);//可以获得删除了分隔符的字符串
Stream tokens = commas.splitAsStream(input);//延迟获取
String[] tokens = input.split("\\s*,\\s*");//不关心匹配模式的预编译和延迟获取,可以直接使用String.split方法
替换则使用replace方法
标记
Pattern pattern = Pattern.compile(regex,Pattern.CASE_INSENSITIVE|PATTERN.UNICODE_CHARACTER_CLASS);
其中CASE_INSENSITIVE表示大小写不敏感,只考虑ASCII码
UNICODE_CASE表示使用UNICODE字母
UNICODE_CHARACTER_CLASS表示设置成Unicode字符群模式而不是POSIX
MULTILINE表示使得开头字符^和末尾字符$匹配行开头和末尾,而不是整个字符
UNIX_LINES多行模式下匹配开头^和末尾$时只认'\n'为结束符
DOTALL或s,使得字符.匹配所有字符包括行结束符
COMMENTS或x,忽略空白符和注释
LITERAL,除了可能忽略大小写外严格匹配字符
CANON_EQ,匹配规范相等的Unicode字符,例如u后跟''(分音符)就匹配ü
序列化
用于将一个对象转化为字节方便传送到别处或者存储到硬盘上,并且能再从转化成的字节重构对象的机制
Serializable接口
举例
public class Employee implements Serializable{
private String name;
private double salary;
}
当所有实例变量都是基本类型或枚举类型时,或者引用其他可序列化对象,那么实现Serializable接口就是安全并合适的,
要序列化对象则需要使用
ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(path));
out.writeObject(employee);
读回序列化对象
ObjectInputStream in = new ObjectInputStream(Files.newInputStream(path));
Employee e = (Employee)in.readObject();
对象在被写入时,类名和所有实例变量的名称和值都被保存,如果实例变量是基本类型,会以二进制形式保存数据。
值得注意的是如果两个对象引用同一个对象,例如两个员工的老板是同一个人,则在读取回这个对象时,两个老板也是同一个人,而不是内容相同的不同对象
所以序列化的过程中会为每一个对象保存时记录一个序列号,当传递对象引用到writeObject是,ObjectOutputStream会检查对象是否在引用前已经被写入,如果已经写入则只写序列号而不复制对象内容
瞬态实例变量
为了实现某些实例变量不被序列化,可以为该变量打上transient修饰符标记,主要用于缓存值、数据库连接等
重写readObject和writeObject方法来自定义序列化机制
public class LabeledPoint implements Serializable{
private String lable;
private transient Point2D point;//没有默认的序列化方法
private void writeObject(ObjectOutputStream out)
throws IOException{
out.defaultWriteObject();
out.writeDouble(point.getX());
out.writeDouble(point.getY());
}
private void readObject(ObjectInputStream in)
throws IOException,ClassNotFoundException{
in.defaultReadObject();
double x = in.readDouble();
double y = in.readDouble();
point = new Point2D(x,y);
}
}
值得注意的是readObject和writeObject方法只需读写自己的实例变量,而不应该涉及父类的数据
ReadResolve和writeReplace方法
readResolve方法解决了单例模式,在反序列化的时候会出现两个完全相同的对象而引发的问题,readResolve方法在反序列化时直接提供指定的对象。
public final class MySingleton{
private MySingleton(){}
private static final MySingleton INSTANCE = new MySingleton();
public static MySingleton getInstance(){return INSTANCE;}
private Object readResolve() throws ObjectStreamException{
return INSTANCE;
}
}
writeReplace方法会在序列化的时候将当前对象替换成另外一个对象(并返回替换后的对象)并写入流中。恢复的时候没有自动回复方法,仅能提取到替换后的对象,需要手动恢复成之前的对象。
版本化
序列化机制支持一个简单的版本模式,当一个对象被序列化是,包括类名和它的serialVersionUID信息会被写入对象流,通过定义一个实例变量,实现器会分配一个唯一标识。
private static final long serialVersionUID = 1L;//版本1
当以不兼容的方式进行演化时,实现者就需要改变其UID,当反序列化对象有一个不匹配的UID时,readObject会抛出InvalidClassException异常。
如果匹配时,则反序列化正常处理,处理到变化值会被设置为默认值,对象为null,数值为0,布尔值为fasle