高度な Java - IO ストリーム「Diverse Streams」の第 2 部

目次

Java バッファリングされたストリーム

BufferedReader クラスと BufferedWriter クラス

Javaのランダムストリーム

Java配列ストリーム

バイト配列ストリーム

ByteArrayInputStream ストリームの構築方法:

ByteArrayOutputStream ストリームの構築方法:

文字配列ストリーム

Javaデータフロー

Javaオブジェクトストリーム

Java のシリアル化とオブジェクトのクローン作成

ちょっとした知識の拡張:

Java はスキャナーを使用してファイルを解析します

デフォルトの区切られたトークンを使用してファイルを解析する

正規表現を区切りトークンとして使用してファイルを解析する

Javaファイルダイアログ

プログレスバー付きの Java 入力ストリーム

Javaファイルロック


Java バッファリングされたストリーム

バッファリングされたストリームは、データの読み取りと書き込みの効率を向上させるためのバッファーを提供する Java のストリームです。バッファ ストリームは、データをメモリ内のバッファに一時的に保存し、一度にデータの書き込みまたは読み取りを行うことができるため、頻繁なディスク操作やネットワーク操作が軽減され、データの読み取りと書き込みの速度が向上します。

バッファリングされたストリームでは次のことができます。

  1. 読み取りおよび書き込みの効率の向上: バッファ ストリームにより、ディスクまたはネットワークの操作の数が削減され、最初にメモリ内のバッファにデータが格納され、その後一度に書き込みまたは読み取りが行われるため、データの読み取りおよび書き込みの効率が向上します。
  2. 追加機能の提供: バッファリングされたストリームは、文字エンコード変換のサポート、行操作のサポートなど、いくつかの追加機能も提供します。

Java では、一般的なバッファリングされたストリームは次のとおりです。

  1. BufferedInputStream: これは、データの読み取り効率を向上させるバッファを提供する、InputStream のラッパーです。一度に複数のバイトをバッファに読み取り、バッファからバイトごとに読み取ることができるため、頻繁なディスク操作やネットワーク操作が軽減されます。BufferedInputStream を使用すると、大量のデータの読み取り効率が向上します。
  2. BufferedOutputStream: OutputStream のラッパーであり、データ書き込みの効率を向上させるバッファーを提供します。最初にデータをバッファに書き込んでから、ターゲット出力ストリームに一度に書き込むことができるため、頻繁なディスク操作やネットワーク操作が軽減されます。BufferedOutputStream を使用すると、大量のデータの書き込み効率が向上します。
  3. BufferedReader: 文字データの読み取り効率を向上させるバッファを提供する Reader のラッパーです。一度に複数の文字をバッファに読み取り、バッファから 1 文字ずつ読み取ることができるため、頻繁なディスク操作やネットワーク操作が軽減されます。BufferedReader を使用すると、大量の文字データの読み取り効率が向上します。
  4. BufferedWriter: Writer のラッパーであり、文字データの書き込み効率を向上させるバッファーを提供します。最初に文字データをバッファに書き込んでから、ターゲット出力ストリームに一度に書き込むことができるため、頻繁なディスク操作やネットワーク操作が軽減されます。BufferedWriter を使用すると、大量の文字データの書き込み効率を向上させることができます。

これらのバッファリングされたストリームは、バイト ストリームまたは文字ストリームのラッパーです。構築メソッドを通じてバイト ストリームまたは文字ストリームをバッファリングされたストリームに渡し、バッファリングされたストリームを通じて読み取りおよび書き込み操作を実行できます。

BufferedReaderクラスとBufferedWriterクラス

Java では、 BufferedReaderクラスとBufferedWriterクラスによって作成されたオブジェクトを、バッファリングされた入力ストリームと出力ストリームと呼びます。これにより、ファイルの読み取りと書き込みの機能が強化されます。たとえば、Sudent.txt は学生のリストであり、各名前が 1 行を占めています。名前を読みたい場合は、一度に 1 行を読み取る必要があります。1 行に何文字入っているかが分からず、FileReader クラスがそれを行うため、FileReader ストリームを使用してそのようなタスクを完了するのは困難です。行を読み取るメソッドは提供されていません。

Java は、より高度なストリームである BufferedReader ストリームと BufferedWriter ストリームを提供します。両方のソースと宛先は文字入力ストリームと文字出力ストリームである必要があります。したがって、ファイル文字入力ストリームが BufferedReader ストリームのソースとして使用され、ファイル文字出力ストリームが BufferedWriter ストリームの宛先として使用される場合、BufferedReader クラスと BufferedWriter クラスによって作成されたストリームの読み取りと書き込みが強化されます。文字入力ストリームおよび文字出力ストリームよりも優れた機能。たとえば、BufferedReader ストリームはファイルを 1 行ずつ読み取ることができます。

BufferedReader クラスと BufferedWriter の構築メソッドは次のとおりです。 

BufferedReader(Reader in);
BufferedWriter(Writer out);

BufferedReader と BufferedWriter を上流ストリーム、それらが指す文字ストリームを下流ストリームと呼びます。Java はキャッシュ技術を使用して上流ストリームと下流ストリームを接続します。基礎となる文字入力ストリームは最初にデータをバッファに読み取り、BufferedReader ストリームはバッファからデータを読み取り、BufferedWriter ストリームはデータをバッファに書き込み、基礎となる文字出力ストリームはバッファ内のデータを宛先に継続的に書き込みます。BufferedWriter ストリームが flash() を呼び出してキャッシュを更新するか、close() メソッドを呼び出して閉じると、バッファがオーバーフローしなくても、基になるストリームはキャッシュされたコンテンツをただちに宛先に書き込みます。

:出力ストリームを閉じるときは、まずバッファリングされた出力ストリームを閉じ、次にバッファリングされた出力ストリームが指すストリームを閉じます。つまり、最初に上流ストリームを閉じてから、下位ストリームを閉じます。コードを記述するときに上流ストリームを閉じるだけで、上流ストリームの基になるストリームが自動的に閉じられます。

例えば:

英語の文章で構成されるファイル a.txt は次のとおりで、各文章は 1 行を占めます。

hello
I like you
no yes

a.txt を 1 行ずつ読み取り、その行の後に英語の文に含まれる単語の数を追加し、その行を b.txt という名前のファイルに書き込む必要があります。コードは次のとおりです。

import java.io.*;
import java.util.*;
public class Main {
    public static void main(String args[]) {
        File fRead = new File("a.txt");
        File fWrite = new File("b.txt");
        try {
            Writer out = new FileWriter(fWrite);
            BufferedWriter bufferWrite = new BufferedWriter(out);
            Reader in = new FileReader(fRead);
            BufferedReader bufferRead = new BufferedReader(in);
            String str;
            while((str = bufferRead.readLine()) !=null) {
                StringTokenizer fenxi = new StringTokenizer(str);
                int count = fenxi.countTokens();
                str = str+"句子中单词个数:"+count;
                bufferWrite.write(str);
                bufferWrite.newLine();
            }
            bufferWrite.close();
            out.close();
            in = new FileReader(fWrite);
            bufferRead = new BufferedReader(in);
            String s = null;
            System.out.println(fWrite.getName()+"内容:");
            while((s=bufferRead.readLine()) !=null) {
                System.out.println(s);
            }
            bufferRead.close();
            in.close();
        }
        catch(IOException e) {
            System.out.println(e.toString());
        }
    }
}

操作の結果は次のようになります。

b.txt内容:
hello句子中单词个数:1
I like you句子中单词个数:3
no yes句子中单词个数:2

Javaのランダムストリーム

RandomAccessFile クラスによって作成されたストリームは、ランダム ストリームと呼ばれます。前述の入力および出力ストリームとは異なり、RandomAccessFile クラスは、InputStream クラスのサブクラスでも、OutputStream クラスのサブクラスでもありません。ただし、RandomAccessFile クラスによって作成されたストリームは、ストリームのソースとストリームの宛先の両方として使用できます。つまり、ファイルの読み取りと書き込みの準備をするときに、ファイルを指すランダム ストリームを作成するだけです。ファイル内のデータはこのストリームから読み取ることができ、このストリームを通じてデータをファイルに書き込むことができます。

注: RandomAccessFile はスレッドセーフであるため、複数の RandomAccessFile オブジェクトを同時に開いて同じファイルを読み取ることができます。ただし、複数のスレッドで同時にファイルを読み込むと、データの不整合などが発生する可能性があるので注意してください。

以下は、RandomAccessFile クラスの 2 つのコンストラクターです。

  1. RandomAccessFile(String name,String mode): パラメータ名はファイル名を決定するために使用され、作成されたストリームのソースとストリームの宛先も指定されます。パラメータ モードはr (読み取り専用) またはrw (読み取りおよび書き込み可能) で、これによって、作成されたストリームのファイルへのアクセス権が決まります。
  2. RandomAccessFile(ファイル ファイル、文字列モード): パラメーター ファイルは、作成されたストリームのソースと宛先を与える File オブジェクトです。パラメータ モードはr (読み取り専用) またはrw (読み取りおよび書き込み可能) で、これによって、作成されたストリームのファイルへのアクセス権が決まります。

: RandomAccessFile ストリームがファイルを指している場合、そのファイルは更新されません。

RandomAccessFile クラスには、RandomAccessFile ストリームの読み取りおよび書き込み位置を特定するためのメソッド look(long a) があります。パラメータ a は、読み取りおよび書き込み位置とファイルの先頭の間のバイト数を決定します。さらに、ストリームは getFilePointer() メソッドを呼び出して、ストリームの現在の読み取りおよび書き込み位置を取得することもできます。RandomAccessFile ストリームは、ファイルを順次に読み書きするよりも柔軟に読み書きします。

RandomAccessFile ストリームの一般的なメソッドは次のとおりです。

方法 説明する
近い() ファイルを閉じる
getFilePointer() 現在の読み取りおよび書き込み位置を取得します
長さ() ファイルの長さを取得する
読む() ファイルからデータのバイトを読み取ります
readBoolean() ファイルからブール値を読み取ります。0 は false を意味し、その他の値は true を意味します
readByte()

ファイルからバイトを読み取る

readChar() ファイルから文字 (2 バイト) を読み取ります
readDouble() ファイルから倍精度浮動小数点値(8バイト)を読み込みます
readFloat() ファイルから単精度浮動小数点値 (4 バイト) を読み取ります
readFully(バイト b[ ]) b.length バイトを配列 b に読み取り、配列を完全に埋めます
readInt() ファイルから int 値 (4 バイト) を読み取ります
読み込まれた行() ファイルからテキスト行を読み取る
readLong() ファイルからlong値(8バイト)を読み取ります。
readShort() ファイルから短い値 (2 バイト) を読み取ります
readUnsignedByte() ファイルから符号なしバイト (1 バイト) を読み取ります
readUnsignedShort() ファイルから符号なしの short 値 (2 バイト) を読み取ります
readUTF() ファイルから UTF 文字列を読み取る
シーク(ロングポジション) 読み取りおよび書き込み位置の位置
setLength(長い新しい長さ) ファイルの長さを設定します
スキップバイト(int n) ファイル内の指定されたバイト数をスキップします
書き込み(バイトb[]) b.length バイトをファイルに書き込みます
writeBoolean(ブール値 v) ブール値をシングルバイト値としてファイルに書き込みます
writeByte(int v) ファイルにバイトを書き込む
writeBytes(文字列) 文字列をファイルに書き込む
writeChar(char c) ファイルに文字を書き込む
writeChars(文字列) 文字列を文字データとしてファイルに書き込みます
writeDouble(ダブルv) 倍精度浮動小数点値をファイルに書き込みます
writeFloat(float v) 単精度浮動小数点値をファイルに書き込みます
writeInt(int v) int 値をファイルに書き込みます
writeLong(long v) ファイルにlong int値を書き込みます
writeShort(int v) short int 値をファイルに書き込みます
writeUTF(文字列) UTF文字列を書き込みます

: RandomAccessFile ストリームの readLine() メソッドが、漢字を含むファイルなど、非 ASCII 文字を含むファイルを読み取ると、「文字化け」が発生します。したがって、readLine() によって読み取られた文字列を「iso-8859-1」エンコードを使用してバイト配列に再エンコードし、その後、現在のマシンのデフォルトのエンコードを使用して配列を文字列に変換する必要があります。操作は次のとおりです。

  1. 読む
    String str = in.readLine();
  2. 用“iso-8859-1”重新编码
    byte b[] = str.getBytes("iso-8859-1");
  3. 使用当前机器的默认编码将字节数组转化为字符串
    String content = new String(b);
    如果机器的默认编码是“GB2312”,那么
    String content = new String(b);
    等同于
    String content = new String(b,"GB2312");

随机流代码示例: 

import java.io.*;
public class Main {
    public static void main(String args[]) {
        RandomAccessFile inAndOut = null;
        int data[] = {1,2,3,4,5,6,7,8,9,10};
        try {
            inAndOut = new RandomAccessFile("a.txt","rw");
            for(int i=0;i<data.length;i++) {
                inAndOut.writeInt(data[i]);
            }
            for(long i = data.length-1;i>=0;i--) {
                inAndOut.seek(i*4);
                System.out.printf("\t%d",inAndOut.readInt());
                /*
                一个int型数据占4个字节,inAndOut从文件的第36个字节读取最后面的一个整数,每隔4个字节往前读取一个整数
                */
            }
            inAndOut.close();
        }
        catch(IOException e) {}
    }
}

Java数组流

我们要知道,流的源和目的地除了可以是文件以外,还可以是计算机内存

字节数组流

字节数组输入流ByteArrayInputStream和字节数组输出流ByteArrayOutputStream分别使用字节数组作为流的源和目的地。

ByteArrayInputStream流的构造方法:

ByteArrayInputStream(byte[] buf);
ByteArrayInputStream(byte[] buf,int offset,int length);

第一个构造方法构造的字节数组流的源是参数buf指定的数组的全部字节单元。

第二个构造方法构造的字节数组流的源是buf指定的数组从offset处按顺序取的length个字节单元。

字节数组输入流调用public int read();方法可以顺序地从源中读出一个字节,该方法返回读出的字节值;调用public int read(byte[] b,int off,int len);方法可以顺序地从源中读出参数len指定的字节数,并将读出的字节存放到参数b指定的数组中,参数off指定数组b存放读出字节的起始位置,该方法返回实际读出的字节个数,如果未读出字节read方法返回-1。

ByteArrayOutputStream流的构造方法:

ByteArrayOutputStream();
ByteArrayOutputStream(int size);

第一个构造方法构造的字节数组输出流指向一个默认大小为32字节的缓冲区,如果输出流向缓冲区写入的字节个数大于缓冲区时,缓冲区的容量会自动增加。

第二个构造方法构造的字节数组输出流指向的缓冲区的初始大小由参数size指定,如果输出流向缓冲区写入的字节个数大于缓冲区时,缓冲区的容量会自动增加。

字节数组输出流调用public void write(int b);方法可以顺序地向缓冲区写入一个字节;调用public void write(byte[ ] b,int off,int len);方法可以将参数b中指定的len个字节顺序地写入缓冲区,参数off指定从b中写出的字节的起始位置;调用public byte[ ] toByteArray();方法可以返回输出流写入到缓冲区的全部字节。

字符数组流

与字节数组流对应的是字符数组流CharArrayReader类和CharArrayWriter类,字符数组流分别使用字符数组作为流的源和目标。

例如,使用数组流向内存(输出流的缓冲区)写入“mid-autumn festival”和“中秋快乐”,然后再从内存读取曾写入的数据: 

import java.io.*;

public class Main {
    public static void main(String args[]) {
        try {
            // 创建一个字节数组输出流
            ByteArrayOutputStream outByte = new ByteArrayOutputStream();
            // 将字节内容 "mid-autumn festival" 写入字节数组输出流
            byte [] byteContent = "mid-autumn festival ".getBytes();
            outByte.write(byteContent);

            // 创建一个字节数组输入流,并将字节数组输出流的内容传递给它
            ByteArrayInputStream inByte = new ByteArrayInputStream(outByte.toByteArray());
            // 创建一个与字节数组输出流长度相同的字节数组
            byte backByte [] = new byte[outByte.toByteArray().length];
            // 从字节数组输入流中读取内容到 backByte 数组中
            inByte.read(backByte);
            // 将 backByte 数组转换为字符串并打印出来
            System.out.println(new String(backByte));

            // 创建一个字符数组输出流
            CharArrayWriter outChar = new CharArrayWriter();
            // 将字符内容 "中秋快乐" 写入字符数组输出流
            char [] charContent = "中秋快乐".toCharArray();
            outChar.write(charContent);

            // 创建一个字符数组输入流,并将字符数组输出流的内容传递给它
            CharArrayReader inChar = new CharArrayReader(outChar.toCharArray());
            // 创建一个与字符数组输出流长度相同的字符数组
            char backChar [] = new char [outChar.toCharArray().length];
            // 从字符数组输入流中读取内容到 backChar 数组中
            inChar.read(backChar);
            // 将 backChar 数组转换为字符串并打印出来
            System.out.println(new String(backChar));
        }
        catch(IOException exp) {
            // 处理可能发生的 IOException 异常
            exp.printStackTrace();
        }
    }
}

Java数据流

DataInputStream和DataOutputStream类创建的对象称为数据输入流数据输出流。这两个流是很有用的两个流,它们允许程序按着机器无关的风格读取Java原始数据。也就是说,当读取一个数值时,不必再关心这个数值应当是多少个字节。

DataInputStream和DataOutputStream的构造方法如下:

  1. DataInputStream(InputStream in):创建的数据输入流指向一个由参数in指定的底层输入流。
  2. DataOutputStream(OutputStream out):创建的数据输出流指向一个由参数out指定的底层输出流。

DataInputStreamDataOutputStream类的常用方法如下:

方法 说明
close() 关闭流
readBoolean() 读取一个布尔值
readByte() 读取一个字节
readChar() 读取一个字符
readDouble() 读取一个双精度浮点值
readFloat() 读取一个单精度浮点值
readInt() 读取一个int值
readLong() 读取一个长型值
readShort() 读取一个短型值
readUnsignedByte() 读取一个无符号字节
readUnsignedShort() 读取一个无符号短型值
readUTF() 读取一个UTF字符串
skipBytes(int n) 跳过给定数量的字节
writeBoolean(boolean v) 写入一个布尔值
writeBytes(String s) 写入一个字符串
writeChars(String s) 写入字符串
writeDouble(double v) 写入一个双精度浮点值
writeFloat(float v) 写入一个单精度浮点值
writeInt(int v) 写入一个int值
writeLong(long v) 写入一个长型int值
writeShort(int v) 写入一个短型int值
writeUTF(String s) 写入一个UTF字符串

例如,写几个Java类型的数据到一个文件,然后再读出来: 

import java.io.*;

public class Main {
    public static void main(String[] args) {
        // 写入文件
        try (DataOutputStream outputStream = new DataOutputStream(new FileOutputStream("a.txt"))) {
            int intValue = 10;
            double doubleValue = 3.14;
            String stringValue = "Hello, World!";

            outputStream.writeInt(intValue);
            outputStream.writeDouble(doubleValue);
            outputStream.writeUTF(stringValue);

            System.out.println("数据写入成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 读取文件
        try (DataInputStream inputStream = new DataInputStream(new FileInputStream("a.txt"))) {
            int intValue = inputStream.readInt();
            double doubleValue = inputStream.readDouble();
            String stringValue = inputStream.readUTF();

            System.out.println("读取的整数值:" + intValue);
            System.out.println("读取的浮点数值:" + doubleValue);
            System.out.println("读取的字符串值:" + stringValue);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Java对象流

ObjectInputStream和ObjectOutputStream类分别是InputStream和OutputStream类的子类。ObjectInputStream和ObjectOutputStream类创建的对象称为对象输入流和对象输出流。对象输出流使用writeObject(Object obj)方法将一个对象obj写入到一个文件,对象输入流使用readObject()读取一个对象到程序中。

ObjectInputStream和ObjectOutputStream类的构造方法如下:

  1. ObjectInputStream(InputStream in)
  2. ObjectOutputStream(OutputStream out)

ObjectOutputStream的指向应当是一个输出流对象,因此当准备将一个对象写入到文件时,首先用OutputStream的子类创建一个输出流。 

例如,用FileOutputStream创建一个文件输出流,代码如下:

FileOutputStream fileOut = new FileOutputStream("a.txt");
ObjectOutputStream objectout = new ObjectOutputStream(fileOut);

同样ObjectInputStream的指向应当是一个输入流对象,因此当准备从文件中读入一个对象到程序中时,首先用InputStream的子类创建一个输入流。

例如,用FileInputStream创建一个文件输入流,代码如下:

FileInputStream fileIn = new FileInputStream("a.txt");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);

当使用对象流写入或读入对象时,要保证对象是序列化的,这是为了保证能把对象写入到文件,并能再把对象正确读回到程序中。

一个类如果实现了Serializable接口(java.io包中的接口),那么这个类创建的对象就是所谓序列化的对象。Java类库提供的绝大多数对象都是所谓序列化的。需要强调的是,Serializable接口中没有方法,因此实现该接口的类不需要实现额外的方法。另外需要注意的是,使用对象流把一个对象写入到文件时不仅要保证该对象是序列化的,而且该对象的成员对象也必须是序列化的。

Serializable接口中的方法对程序是不可见的,因此实现该接口的类不需要实现额外的方法,当把一个序列化的对象写入到对象输出流时,JVM就会实现Serializable接口中的方法,将一定格式的文本(对象的序列化信息)写入到目的地。当ObjectInputStream对象流从文件读取对象时,就会从文件中读回对象的序列化信息,并根据对象的序列化信息创建一个对象。

下面是一个使用这两个类进行对象序列化和反序列化的Java代码示例:

import java.io.*;

public class Main {
    public static void main(String[] args) {
        // 序列化对象并写入文件
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("data.obj"))) {
            // 创建一个自定义对象
            Person person = new Person("John Doe", 25);

            // 将对象写入文件
            outputStream.writeObject(person);

            System.out.println("对象序列化成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化对象并读取文件
        try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("data.obj"))) {
            // 从文件中读取对象
            Person deserializedPerson = (Person) inputStream.readObject();

            System.out.println("读取的对象信息:");
            System.out.println("姓名:" + deserializedPerson.getName());
            System.out.println("年龄:" + deserializedPerson.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    // 自定义Person类,需要实现Serializable接口才能被序列化
    static class Person implements Serializable {
        private String name;
        private int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}

上述代码创建了一个名为Person的自定义类,并在其中定义了姓名和年龄属性。该类实现了Serializable接口,使得对象可以被序列化。首先,将一个Person对象序列化并写入到名为"data.obj"的文件中。然后,从文件中反序列化出对象,并进行相关信息的打印输出。

请注意,要想使自定义类能够被序列化,需要确保该类实现了Serializable接口。同时,反序列化过程需要进行类型转换,以恢复原始的对象类型。

Java序列化与对象克隆

Java的序列化和对象克隆都是用于处理对象的机制,但它们有不同的作用和实现方式。

  • 序列化(Serialization):

  1. 序列化用于将对象转换为字节流的过程,以便在网络传输或持久化存储时使用。
  2. 通过实现Serializable接口,类可以支持序列化。该接口没有任何方法,只是一个标记接口。
  3. 使用ObjectOutputStream将对象写入输出流,使用ObjectInputStream从输入流中读取对象。
  4. 序列化过程会将对象的状态以及对象包含的数据保存下来,然后可以重新反序列化为原始对象。
  • 对象克隆(Object Cloning):

  1. 对象克隆用于创建一个现有对象的副本,以便通过克隆对象进行操作,而不影响原对象。
  2. 在默认情况下,Java中的对象克隆是浅克隆,即只复制对象本身,而不复制引用类型的成员变量。
  3. 要实现对象克隆,类必须实现Cloneable接口,并重写clone()方法。
  4. 使用clone()方法可以创建一个新的与原始对象相同的副本。

虽然序列化和对象克隆都涉及对象的复制和重建,但它们的应用场景有所不同:

  • 序列化主要用于对象的传输和持久化存储,例如在网络通信中发送对象、将对象保存到文件或数据库中。
  • 对象克隆主要用于创建一个相同属性的对象副本,在某些场景下可以提高性能、降低开销或简化代码逻辑。

需要注意的是,虽然对象克隆可以方便地创建对象的副本,但它也有一些潜在问题,如浅克隆的风险、深克隆的复杂性等。因此,在使用对象克隆时需要谨慎,并根据具体需求选择合适的实现方式。

扩展小知识:

在编程中,克隆(Clone)和拷贝(Copy)是用于复制对象或数据的概念,但它们有一些区别:

  • 克隆(Clone):

  1. 克隆是创建一个与原始对象具有相同状态和数据的全新对象。
  2. 克隆使用的是对象克隆机制,通过调用对象的 clone() 方法实现。
  3. 克隆得到的对象是独立的,对克隆对象的修改不会影响原始对象。
  4. 在默认情况下,Java 的对象克隆是浅克隆,只复制对象本身,而不复制引用类型的成员变量。如果需要深度克隆,需要在 clone() 方法中手动处理引用类型的成员变量。
  • 拷贝(Copy):

  1. 拷贝是指将一个对象或数据的值复制到另一个对象或数据中。
  2. 拷贝可以通过多种方式进行,包括不同层级的拷贝(如深拷贝和浅拷贝)、手动逐个复制属性等。
  3. 拷贝的实现方式依赖于编程语言或框架提供的特定操作或函数。

区别总结:

  • 克隆是创建一个全新的、与原始对象具有相同状态和数据的对象,通过对象自身的克隆机制实现。
  • 拷贝是将一个对象或数据的值复制到另一个对象或数据中,可以使用不同方式实现。
  • 克隆得到的对象是独立的,对克隆对象的修改不会影响原始对象;而拷贝得到的对象可能与原始对象共享引用数据。
  • 在 Java 中,默认的对象克隆是浅克隆,需要手动处理引用类型的成员变量以实现深度克隆;而拷贝可以根据需求选择不同层级的拷贝方式。

下面是一个使用Java序列化和对象克隆进行对象复制的代码示例: 

import java.io.*;

// 实现Serializable接口以支持序列化
class Person implements Serializable, Cloneable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 省略了Getter和Setter方法

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }

    // 重写clone()方法,实现对象的深拷贝
    @Override
    public Object clone() throws CloneNotSupportedException {
        // 调用父类的clone()方法进行浅拷贝
        Person clonedPerson = (Person) super.clone();
        // 对非基本类型的字段进行拷贝
        clonedPerson.name = new String(this.name);
        return clonedPerson;
    }
}

public class Main {
    public static void main(String[] args) {
        // 使用序列化进行对象复制
        Person person1 = new Person("Alice", 25);

        Person person2 = null;
        try {
            // 将person1序列化为字节流
            ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
            ObjectOutputStream objectOut = new ObjectOutputStream(byteOut);
            objectOut.writeObject(person1);

            // 将字节流反序列化为person2对象
            ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
            ObjectInputStream objectIn = new ObjectInputStream(byteIn);
            person2 = (Person) objectIn.readObject();

            // 通过序列化实现的对象复制完成
            System.out.println("使用序列化进行对象复制:");
            System.out.println("原始对象:" + person1);
            System.out.println("复制后的对象:" + person2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        // 使用对象克隆进行对象复制
        Person person3 = new Person("Bob", 30);

        Person person4 = null;
        try {
            // 调用person3的clone()方法进行对象克隆
            person4 = (Person) person3.clone();

            // 通过对象克隆实现的对象复制完成
            System.out.println("使用对象克隆进行对象复制:");
            System.out.println("原始对象:" + person3);
            System.out.println("复制后的对象:" + person4);
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

注意:clone()方法在java.lang.Object类中是受保护的成员,意味着它只能被同一类或子类中的方法访问。这使得对象克隆只能在类内部或继承关系中使用。在示例代码中,要想成功对Person对象进行克隆,必须在Person类中实现Cloneable接口,并覆盖clone()方法,将其访问控制修改为public。通过在Person类中实现Cloneable接口,并重写clone()方法并将其访问控制修改为public,就可以在示例代码中成功地使用对象克隆进行复制了。

Java使用Scanner解析文件

使用默认分隔标记解析文件

创建Scanner对象,并指向要解析的文件,例如:

File file = new File("a.txt");
Scanner sc = new Scanner(file);

那么sc将空格作为分隔标记,调用next()方法依次返回file中的单词,如果file最后一个单词已被next()方法返回,sc调用hasNext()将返回false,否则返回true。

另外,对于数字型的单词,比如108,167.92等可以用nextInt()或nextDouble()方法来代替next()方法,即sc可以调用nextInt()或nextDouble()方法将数字型单词转化为int或double数据返回,但需要特别注意的是,如果单词不是数字型单词,调用nextInt()或nextDouble()方法将发生InputMismatchException异常,在处理异常时可以调用next()方法返回该非数字化单词。

使用正则表达式作为分隔标记解析文件

创建Scanner对象,指向要解析的文件,并使用useDelimiter方法指定正则表达式作为分隔标记,例如:

File file = new File("a.txt");
Scanner sc = new Scanner(file);
sc.useDelimiter(正则表达式);

那么sc将正则表达式作为分隔标记,调用next()方法依次返回file中的单词,如果file最后一个单词已被next()方法返回,sc调用hasNext()将返回false,否则返回true。

另外,对于数字型的单词,比如1979,0.618等可以用nextInt()或nextDouble()方法来代替next()方法,即sc可以调用nextInt()或nextDouble()方法将数字型单词转化为int或double数据返回,但需要特别注意的是,如果单词不是数字型单词,调用nextInt()或nextDouble()方法将发生InputMismatchException异常,那么在处理异常时可以调用next()方法返回该非数字化单词。

以下是使用Scanner解析文件中的数字,并计算这些数字的平均值的代码示例:

a.txt:

张三的成绩是70分,李四的成绩是80分,赵五的成绩是90分。
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        File file = new File("a.txt");
        Scanner sc = null;
        int count = 0;
        double sum = 0;

        try {
            double score = 0;
            sc = new Scanner(file);
            sc.useDelimiter("[^0123456789.]+");  // 使用非数字字符作为分隔符

            while (sc.hasNextDouble()) {
                score = sc.nextDouble();
                count++;
                sum = sum + score;
                System.out.println(score);
            }

            double average = sum / count;
            System.out.println("平均成绩:" + average);

            // 关闭Scanner
            sc.close();

        } catch (Exception exp) {
            System.out.println(exp);
        }
    }
}

Java文件对话框

文件对话框是一个选择文件的界面。Javax.swing包中的JFileChooser类可以创建文件对话框,使用该类的构造方法JFileChooser()创建初始不可见的有模式文件对话框。然后文件对话框调用下述2个方法:

showSaveDialog(Component a);
showOpenDialog(Component a);

都可以使得对话框可见,只是呈现的外观有所不同,showSaveDialog方法提供保存文件的界面,showOpenDialog方法提供打开文件的界面。上述两个方法中的参数a指定对话框可见时的位置,当a是null时,文件对话框出现在屏幕的中央;如果组件a不空,文件对话框在组件a的正前面居中显示。

用户单击文件对话框上的“确定”、“取消”或“关闭”图标,文件对话框将消失,ShowSaveDialog()showOpenDialog()方法返回下列常量之一:

JFileChooser.APPROVE OPTION
JFileChooser.CANCEL_OPTION

如果希望文件对话框的文件类型是用户需要的几种类型,比如,扩展名是.jpeg等图像类型的文件,可以使用FileNameExtensionFilter类事先创建一个对象,在JDK 1.6版本,FileNameExtensionFilter类在javax.swing.filechooser包中。

下面是一个示例代码片段,演示了如何使用JFileChooser类创建文件对话框,并设置文件类型过滤器:

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;

public class Main {

    public static void main(String[] args) {
        // 创建文件对话框对象
        JFileChooser fileChooser = new JFileChooser();

        // 设置文件类型过滤器
        FileNameExtensionFilter filter = new FileNameExtensionFilter("图像文件", "jpg", "gif");
        fileChooser.setFileFilter(filter);

        // 显示保存文件对话框并获取用户操作的结果
        int result = fileChooser.showSaveDialog(null);

        // 处理用户的操作结果
        if (result == JFileChooser.APPROVE_OPTION) {
            // 用户点击了"确定"按钮
            String selectedFilePath = fileChooser.getSelectedFile().getPath();
            System.out.println("用户选择的文件路径:" + selectedFilePath);
        } else if (result == JFileChooser.CANCEL_OPTION) {
            // 用户点击了"取消"按钮
            System.out.println("用户取消了操作");
        }
    }
}

在上述示例中,我们创建了一个JFileChooser对象,并通过setFileFilter()方法设置文件类型过滤器为指定的图像文件类型(.jpg和.gif)。然后,我们调用showSaveDialog(null)来显示保存文件的文件对话框。根据用户的操作结果,我们可以处理用户选中的文件路径或取消操作的情况。 

Java带进度条的输入流

ProgressMonitorInputStream是一个可以显示读取进度条的输入流类。它可以在文件读取过程中弹出一个进度条窗口来显示读取速度和进度。其构造方法是:

ProgressMonitor InputStream(Conmponent c,String s,InputStream);
  1. 组件c指定了进度条窗口将显示在哪个组件的前面。可以传入一个具体的组件对象,如JFrame或JPanel,进度条会显示在该组件的正前方。如果你传入null,则进度条将显示在屏幕的正前方。
  2. 字符串s是进度条窗口的标题,用于描述正在进行的操作。可以根据需要给进度条窗口设置一个有意义的标题。
  3. 输入流InputStream是要读取的文件的输入流。通过将文件输入流传给ProgressMonitorInputStream,你可以在读取文件时实时显示进度条并监控读取的进度。

需要注意的是,ProgressMonitorInputStream属于javax.swing包,所以在使用之前需要确保已经导入该包。另外,为了使进度条能够正常显示,需要在图形界面线程中执行文件的读取操作。 

import javax.swing.*;
import java.io.*;

public class Main {
    public static void main(String args[]) {
        byte b[] = new byte[1024]; // 增加缓冲区的大小,以便更好地读取文件内容
        try {
            FileInputStream input = new FileInputStream("a.txt");

            // 创建进度条窗口的父组件(例如:JFrame)
            JFrame frame = new JFrame();

            ProgressMonitorInputStream in = new ProgressMonitorInputStream(
                    frame, "读取txt", input);
            ProgressMonitor p = in.getProgressMonitor(); // 获得进度条

            int bytesRead; // 用来记录每次读取到的字节数
            while ((bytesRead = in.read(b)) != -1) {
                String s = new String(b, 0, bytesRead); // 仅使用读取到的字节构建字符串
                System.out.print(s);
                Thread.sleep(1000); // 为了看清进度条,延迟了一定时间
            }

            in.close();

            // 关闭进度条窗口
            frame.dispose();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Java文件锁

通过使用文件锁,可以确保多个程序对同一个文件进行处理时不会发生混乱。Java在JDK 1.4版本后提供了文件锁功能,可以通过FileLock和FileChannel类来实现。

下面是使用文件锁的基本步骤:

  1. 使用RandomAccessFile流建立指向文件的流对象,并设置读写属性为"rw"。
    RandomAccessFile input = new RandomAccessFile("a.txt","rw");
  2. input流调用方法getChannel()获得一个连接到底层文件的FileChannel对象(信道)。
    FileChannel channel = input.getChannel();
  3. 信道调用tryLock()或lock()方法获得一个FileLock(文件锁)对象,这一过程也称做对文件加锁。
    FileLock lock = channel.tryLock();

文件锁对象产生后,将禁止任何程序对文件进行操作或再进行加锁。对一个文件加锁之后,如果想读、写文件必须让FileLock对象调用release()释放文件锁。

lock.release();

以下是一个代码示例,演示如何使用文件锁进行文件读写: 

import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class Main {
    public static void main(String[] args) {
        RandomAccessFile file = null;
        FileChannel channel = null;
        FileLock lock = null;

        try {
            // 1. 创建RandomAccessFile流对象并设置读写属性为"rw"
            file = new RandomAccessFile("a.txt", "rw");

            // 2. 获取FileChannel对象
            channel = file.getChannel();

            while (true) {
                try {
                    // 3. 尝试加锁
                    lock = channel.tryLock();
                    if (lock != null) {
                        // 文件已被锁定
                        System.out.println("文件已被锁定");
                        // 进行文件读取操作
                        String line = file.readLine();
                        System.out.println("读取的文本: " + line);
                        // 释放文件锁
                        lock.release();
                        System.out.println("释放文件锁");
                        // 继续下一轮循环
                        continue;
                    }
                } catch (Exception e) {
                    // 加锁失败
                    e.printStackTrace();
                }

                // 等待一段时间后再尝试加锁
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (file != null)
                    file.close();
                if (channel != null)
                    channel.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

该示例中,首先创建一个RandomAccessFile对象并设置读写属性为"rw"。然后通过调用getChannel()方法获取与文件连接的FileChannel对象。进入循环后,程序会尝试对文件进行加锁,并检查是否成功获取到FileLock对象。如果成功获取到锁,则进行文件读取操作,读取完毕后释放文件锁。如果未能获取到锁,则等待一段时间后再次尝试。

おすすめ

転載: blog.csdn.net/m0_74293254/article/details/132411710