Java编程思想笔记0x10

Java I/O 系统(六)

对象序列化

  • Java的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。这一过程甚至可通过网络进行;这意味着序列化机制能自动弥补不同操作系统之间的差异。
  • 对象序列化可以实现轻量级持久性。持久性值为准一个对象的生存周期并不取决于程序是否在执行,而是可以生存与程序调用之间。之所以称其为轻量级,是因为不能用某种持久的关键字来简单地定义一个对象,并让系统自动维护其它细节问题,相反,对象必须在程序中显示地序列化和反序列化。
  • 对象序列化可以用于Java的远程方法调用和Java Beans对象序列化。

实现RMI的主要步骤:

  1. 定义一个远程接口,此接口需要继承Remote
  2. 开发远程接口的实现类
  3. 创建一个server并把远程对象注册到端口
  4. 创建一个client查找远程对象,调用远程方法、

Java Bean类是指Java中遵循关于命名、构造器、方法的特定规范的一种类型,以提供通用性。

  • 只要对象实现了Serializable接口就可以进行对象序列化。Serializable是一个标记接口,其中没有任何方法。
  • 序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内,然后调用writeObject()即可将对象序列化,并将其发送给OutputStream;反序列化一个对象,需要将一个InputStream对象封装在ObjectInputStream中,然后调用readObject(),此时获得一个引用,指向一个向上转型的Object,需要进行合适的向下转型才能使用。
  • 对象序列化不仅保存了对象本身的信息,而且能最终对象内所包含的所有引用,并保存那些对象;接着对这些对象继续追踪它们包含的引用并保存,以此类推。

寻找类

  • 必须保证Java虚拟机能够找到序列化对象对应类的.class文件,否则会出现ClassNotFoundException
class Alien implements Serializable {}
public class Main {
    public static void main(String[] args) throws Exception {
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream("x.file"));
        Alien a = new Alien();
        out.writeObject(a);
    }
}

此时再编写反序列化部分,和Alien不在同一目录下。

public class Test {
    public static void main(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("x.file"));
        Object a = in.readObject();
        System.out.println(a.getClass());
    }
}
/* Output:
Exception in thread "main" java.lang.ClassNotFoundException: Alien
...
*/

如果前后序列化的类名字相同,但是成员有变化会出现异常。

// 序列化时定义
class Alien implements Serializable {
    int i = 1;
}
// 反序列化时定义
class Alien implements Serializable {
    String i = "1";
}

此时进行反序列化会出现:

Exception in thread "main" java.io.InvalidClassException: Alien; local class incompatible: stream classdesc serialVersionUID = 185057998004728250, local class serialVersionUID = 6323316111603380755

但是如果成员名称和权限相同,成员变量仅赋值不同,成员方法的返回值和参数列表均相同但方法内部实现不同则不会出现异常,例如:

// 序列化时定义
class Alien implements Serializable {
    int k = 0;
    public int a() {
        int i = 0;
        i++;
        return i;
    }
}
// 反序列化时定义
class Alien implements Serializable {
    int k = 566;
    public int a() {
        return 10;
    }
}

此时进行反序列化不会出现异常。

序列化控制

  • 如果希望对序列化过程进行控制,可以通过实现Externalizable接口代替Serializable
public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println("Constructing objects...");
        Blip1 b1 = new Blip1();
        Blip2 b2 = new Blip2();
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("blips.out"));
        System.out.println("Saving objects...");
        o.writeObject(b1);
        o.writeObject(b2);
        o.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("blips.out"));
        System.out.println("Recovering b1...");
        b1 = (Blip1) in.readObject();
        System.out.println("Recovering b2...");
        b2 = (Blip2) in.readObject();
        in.close();;
    }
}
class Blip1 implements Externalizable {
    public Blip1() {
        System.out.println("Blip1 Constructor");
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blip1#writeExternal");
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("Blip1#reeadExternal");
    }
}
class Blip2 implements Externalizable {
    Blip2() {
        System.out.println("Blip2  Constructor");
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blip2#writeExternal");
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("Blip2#reeadExternal");
    }
}
/* Output:
Constructing objects...
Blip1 Constructor
Blip2  Constructor
Saving objects...
Blip1#writeExternal
Blip2#writeExternal
Recovering b1...
Blip1 Constructor
Blip1#reeadExternal
Recovering b2...
Exception in thread "main" java.io.InvalidClassException: Blip2; no valid constructor
*/
  • ExternalizableSerializable不同,后者完全以对象存储的二进制位来构造,而不需要构造器,前者则会调用类所有默认的构造器(包括字段定义的初始化),然后调用readExternal()
public class Blip3 implements Externalizable {
    private int i;
    private String s;
    public Blip3() {
        System.out.println("Blip3 Constructor");
    }
    public Blip3(String x, int a) {
        s = x;
        i = a;
    }
    public String toString() {
        return s + i;
    }
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Blip3#writeExternal");
        // 可以控制序列化成员
        out.writeObject(s);
        out.writeInt(i);
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 按照writeExternal中写入顺序读出
        s = (String) in.readObject();
        i = in.readInt();
    }
    public static void main(String[] args) throws Exception {
        System.out.println("Constructing objects...");
        Blip3 b3 = new Blip3();
        System.out.println(b3);
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("b3.out"));
        System.out.println("Saving object...");
        o.writeObject(b3);
        o.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("b3.out"));
        System.out.println("Recovering object...");
        b3 = (Blips3) in.readObject();
        System.out.println(b3);
    }
}
  • 在上面代码中,如果在writeExternal()readExternal()不保存和恢复成员变量,那么反序列化对象后,其成员变量值均为初始值(i0snull)。
  • 如果不希望对象中某个变量被序列化,除了实现Externalizable接口以外,可以使用Serializable配合关键字transient,该关键字修饰的成员变量在序列化时会被忽略。例如:
private transient String password;

无论在使用中对password赋任何值,在序列化和反序列化后,其值一定为null

  • 尽管Serializable中没有定义方法,但是如果在实现该接口的类中添加writeObject(ObjectOutputStream out)readObject(ObjectInputStream in),那么对该类的对象进行序列化和反序列化时就会调用这两个方法,无论其权限如何(即使是private也会被调用)。此处使用的是反射搜索方法,而不是检查接口。

使用持久性

class House implements Serializable {}
class Animal implements Serializable {
    private String name;
    private House preferredHouse;
    Animal(String nm, House h) {
        name = nm;
        preferredHouse = h;
    }
    public String toString() {
        return name + "[" + super.toString() + "], " + preferredHouse + "\n";
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        House house = new House();
        List<Animal> animals = new ArrayList<>();
        animals.add(new Animal("dog", house));
        animals.add(new Animal("hamster", house));
        animals.add(new Animal("cat", house));
        System.out.println(animals);
        ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
        ObjectOutputStream o1 = new ObjectOutputStream(buf1);
        o1.writeObject(animals);
        // 写第2次
        o1.writeObject(animals);
        ByteArrayOutputStream buf2 = new ByteArrayOutputStream();
        ObjectOutputStream o2 = new ObjectOutputStream(buf2);
        o2.writeObject(animals);
        ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1.toByteArray()));
        ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2.toByteArray()));
        List a1 = (List)in1.readObject();
        List a2 = (List)in1.readObject();
        List a3 = (List)in2.readObject();
        System.out.println(a1);
        System.out.println(a2);
        System.out.println(a3);
    }
}
/* Output:
[dog[Animal@5b464ce8], House@2d3fcdbd
, hamster[Animal@617c74e5], House@2d3fcdbd
, cat[Animal@6537cf78], House@2d3fcdbd
]
[dog[Animal@4501b7af], House@523884b2
, hamster[Animal@5b275dab], House@523884b2
, cat[Animal@61832929], House@523884b2
]
[dog[Animal@4501b7af], House@523884b2
, hamster[Animal@5b275dab], House@523884b2
, cat[Animal@61832929], House@523884b2
]
[dog[Animal@29774679], House@3ffc5af1
, hamster[Animal@5e5792a0], House@3ffc5af1
, cat[Animal@26653222], House@3ffc5af1
]
*/
  • 可以发现,首先对象序列化实现了对象的深拷贝。此外,在o1中写入两次的对象地址是相同的,这意味着在同一个对象序列化流中,系统能够识别出相同的引用。
abstract class Shape implements Serializable {
    public static final int RED = 1, BLUE = 2, GREEN = 3;
    private int xPos, yPos, dimension;
    private static Random rand = new Random(47);
    private static int counter = 0;

    public abstract void setColor(int newColor);

    public abstract int getColor();

    public Shape(int x, int y, int dim) {
        xPos = x;
        yPos = y;
        dimension = dim;
    }

    public String toString() {
        return getClass() + "color[" + getColor() + "] xPos[" + xPos + "] yPos" + yPos + "] dim[" + dimension + "]\n";
    }

    public static Shape randomFactory() {
        int x = rand.nextInt(100);
        int y = rand.nextInt(100);
        int dim = rand.nextInt(100);
        switch (counter++ % 3) {
            default:
            case 0:
                return new Circle(x, y, dim);
            case 1:
                return new Square(x, y, dim);
            case 2:
                return new Line(x, y, dim);
        }
    }
}

class Circle extends Shape {
    private static int color = RED;

    @Override
    public void setColor(int newColor) {
        color = newColor;
    }

    @Override
    public int getColor() {
        return color;
    }

    public Circle(int x, int y, int dim) {
        super(x, y, dim);
    }
}

class Square extends Shape {
    private static int color;

    public Square(int x, int y, int dim) {
        super(x, y, dim);
        color = RED;
    }

    @Override
    public void setColor(int newColor) {
        color = newColor;
    }

    @Override
    public int getColor() {
        return color;
    }
}

class Line extends Shape {
    private static int color = RED;

    public Line(int x, int y, int dim) {
        super(x, y, dim);
    }

    public static void serializeStaticState(ObjectOutputStream os) throws IOException {
        os.writeObject(color);
    }

    public static void deserializeStaticState(ObjectInputStream os) throws IOException {
        color = os.readInt();
    }

    @Override
    public void setColor(int newColor) {
        color = newColor;
    }

    @Override
    public int getColor() {
        return color;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        List<Class<? extends Shape>> shapeTypes = new ArrayList<>();
        shapeTypes.add(Circle.class);
        shapeTypes.add(Square.class);
        shapeTypes.add(Line.class);
        List<Shape> shapes = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            shapes.add(Shape.randomFactory());
        for (int i = 0; i < 10; i++)
            ((Shape)shapes.get(i)).setColor(Shape.GREEN);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cad.out"));
        out.writeObject(shapeTypes);
        Line.serializeStaticState(out);
        out.writeObject(shapes);
        System.out.println(shapes);
    }
}
// 类定义略
public class Test {
    public static void main(String[] args) throws Exception {
        // 恢复
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("cad.out"));
        List<Class<? extends Shape>> shapeTypes2 = (List<Class<? extends Shape>>)in.readObject();
        Line.deserializeStaticState(in);
        List<Shape> shapes2 = (List<Shape>) in.readObject();
        System.out.println(shapes2);
    }
}
  • 注意,在上面代码中,除了Line对象以外的其它对象的static字段color在反序列化后没有设置正确的值,这也是Line类中两个静态方法serializeStaticState()deserializeStaticState()的作用,手动实现static字段的序列化。

Perferences

  • Perferences提供存储小的、受限的数据集合(基本类型和字符串),存储位置和方法与操作系统相关(Windows存储在注册表中)。

Java编程思想笔记0x0f

Java I/O 系统(五)

压缩

  • Java I/O类库中的类支持读写压缩格式的数据流。压缩类库是按字节方式处理的,因此属于InputStreamOutputStream继承层次结构的一部分。
  • 可以使用InputStreamReaderOutputStreamWriter在两种类型之间进行转换。

GZIP

  • 使用GZIP压缩,直接将输出流封装成GZIPOutputStreamZipOutputStream,解压就将输入流封装成GZIPInputStreamZipInputStream
public class GZIPcompress {
    public static void main(String[] args) throws IOException {
        // 检查参数
        if (args.length == 0) {
            System.out.println("no file.");
            System.exit(1);
        }
        // 压缩
        BufferedReader in = new BufferedReader(new FileReader(args[0]));
        BufferedOutputStream out = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream("test.gz")));
        System.out.println("Writing file");
        int c;
        while ((c = in.read()) != -1) 
            out.write(c);
        in.close();
        out.close();
        // 解压
        System.out.println("Reading file");
        BufferedReader in2 = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
        String s;
        while ((s = in2.readLine()) != null)
            System.out.println(s);
    }
}

用Zip进行多文件压缩、解压

public class Main {
    public static void main(String[] args) throws IOException {
        // 压缩
        FileOutputStream f = new FileOutputStream("test.zip");
        CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
        ZipOutputStream zos = new ZipOutputStream(csum);
        BufferedOutputStream out = new BufferedOutputStream(zos);
        for (String arg: args) {
            System.out.print("Writing file " + arg);
            BufferedReader in = new BufferedReader(new FileReader(arg));
            zos.putNextEntry(new ZipEntry(arg));
            int c;
            while ((c = in.read()) != -1)
                out.write(c);
            in.close();
            out.flush();
        }
        out.close();
        // 校验和
        System.out.println("Checksum: " + csum.getChecksum().getValue());
        // 解压
        FileInputStream fi = new FileInputStream("test.zip");
        CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());
        ZipInputStream in2 = new ZipInputStream(csumi);
        BufferedInputStream bis = new BufferedInputStream(in2);
        ZipEntry ze;
        while ((ze = in2.getNextEntry()) != null) {
            System.out.println("Reading file: " + ze);
            int x;
            while ((x = bis.read()) != -1)
                System.out.write(x);
        }
        if (args.length == 1)
            System.out.println("Checksum: " + csumi.getChecksum().getValue());
        bis.close();
        // 另一种解压方式
        ZipFile zf = new ZipFile("test.zip");
        Enumeration e = zf.entries();
        while (e.hasMoreElements()) {
            ZipEntry ze2 = (ZipEntry) e.nextElement();
            System.out.println("File: " + ze2);
            // ...
        }
    }
}
  • 对于每一个要加入压缩档案的文件,都必须调用putNextEntry(),并将其传递给一个ZipEntry对象。ZipEntry对象包含了一个功能很广泛的接口,可以获取和设置Zip文件内特定想上所有可利用的数据,例如名字、压缩和未压缩的文件大小、日期、校验和等等。
  • 为了能够解压文件,ZipInputStream提供了一个getNextEntry()方法返回下一个ZipEntry(如果存在)。
  • 为了读取校验和,必须拥有相关联的Checksum对象的访问权限。

Java编程思想笔记0x0e

Java I/O 系统(四)

新I/O

  • java.nio.*包中引入了新的Java I/O类库,其目的在于提高速度。而速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。在交互过程中只需要和缓冲器交互,把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
  • 唯一直接与通道交互的缓冲器是ByteBuffer,可以存储未加工字节的缓冲器。
  • 引入NIO后,FileInputStreamOutputStreamRandomAccessFile被修改,可以产生FileChannel,用于操纵字节流。java.nio.channels.Channels类提供了可以在通道中产生ReaderWriter等字符模式类的方法。
public class Test {
    public static void main(String[] args) throws IOException {
        // FileChannel fc = new FileOutputStream("test.out").getChannel();
        FileOutputStream fos = new FileOutputStream("test.out");
        FileChannel fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes()));
        fc.close();
        fos.close();
        // fc = new RandomAccessFile("test.out", "rw").getChannel();
        RandomAccessFile raf = new RandomAccessFile("test.out", "rw");
        fc = raf.getChannel();
        fc.position(fc.size());
        fc.write(ByteBuffer.wrap("Some more".getBytes()));
        fc.close();
        raf.close();
        // fc = new FileInputStream("test.out").getChannel();
        FileInputStream fis = new FileInputStream("test.out");
        fc = fis.getChannel();
        ByteBuffer buff = ByteBuffer.allocate(1024);
        fc.read(buff);
        buff.flip();
        while(buff.hasRemaining())
            System.out.print((char)buff.get());
        fc.close();
        fis.close();
    }
}
  • getChannel()会产生一个FileChannel,可以向它传送用于读写的ByteBuffer,并且可以锁定文件的某些区域用于独占式访问。
  • 对于只读访问,必须显式地使用静态allocate()方法来分配ByteBufferByteBuffer的大小关乎I/O的速度。使用allocateDirect()可以产生与操作系统有更高耦合性的直接缓冲器,以获得更高的速度。但是这种分配的开支会更大,并且具体实现与操作系统相关。
public class ChannelCopy {
    private static final int BSIZE = 1024;
    public static void main(String[] args) throw Exception {
        if(args.length !=2) {
            System.out.println("arguments: sourcefile destfile");
            System.exit(1);
        }
        FileInputStream in = new FileInputStream(args[0]);
        FileOutputStream out = new FileOutputStream(args[1]);
        FileChannel fi = in.getChannel(), fo = out.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
        while((in.readf(buffer)) != -1) {
            buffer.flip();
            fo.write(buffer);
            buffer.clear();
        }
    }
}
  • 一旦调用read()来告知FileChannelByteBuffer存储字节,就必须调用缓冲器上的flip(),准备好让其它对象来读取其内容。如果打算继续使用缓冲器执行read(),则必须使用clear()
  • FileChannel#read()返回-1时即达到了输入的末尾。
  • transferTo()transferFrom()可以将一个通道和另一个通道相连。

转换数据

public class Test {
    public static void main(String[] args) throws IOException {
        // 直接输入输出
        FileOutputStream fos = new FileOutputStream("data2.txt");
        FileChannel fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes()));
        fc.close();
        fos.close();
        FileInputStream fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        ByteBuffer bb = ByteBuffer.allocate(1024);
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        System.out.println("-----");
        bb.rewind();
        // 输出时解码
        String encoding = System.getProperty("file.encoding");
        System.out.println("encoding: " + encoding);
        System.out.println(Charset.forName(encoding).decode(bb));
        System.out.println("-----");
        fis.close();
        // 输入时编码
        fos = new FileOutputStream("data2.txt");
        fc = fos.getChannel();
        fc.write(ByteBuffer.wrap("Some text".getBytes("UTF-16BE")));
        fc.close();
        fos.close();
        fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        bb.clear();
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        // 使用CharBuffer输出
        fos = new FileOutputStream("data2.txt");
        fc = fos.getChannel();
        bb = ByteBuffer.allocate(24);
        bb.asCharBuffer().put("Some text");
        fc.write(bb);
        fc.close();
        fis.close();
        // 使用CharBuffer输入
        fis = new FileInputStream("data2.txt");
        fc = fis.getChannel();
        bb.clear();
        fc.read(bb);
        bb.flip();
        System.out.println(bb.asCharBuffer());
        fc.close();
        fis.close();
        fos.close();
    }
}
/* Output:
卯浥⁴數
-----
encoding: UTF-8
Some text
-----
Some text
Some text
*/
  • java.nio.CharBuffer有一个toString()方法,可以返回一个包含缓冲器所有字符的字符串。但是缓冲器中容纳的是普通的字节,要将其转为字符,要么在输入时对其进行编码,要么在将其从缓冲器输出时对其进行解码。

获取基本类型

  • ByteBuffer插入基本类型数据,可以利用asCharBuffer()asShortBuffer()等获得该缓冲器上的视图,然后使用视图的put()方法。注意使用ShortBufferput()方法时需要进行强制类型转换,而其它所有的数据缓冲器在使用put()方法时,不要进行转换。

视图缓冲器

  • 视图缓冲器可以通过某个特定的基本数据类型的试穿查看其底层的ByteBuffer。此外还可以从ByteBuffer一次一个或者成批(数组)读取基本类型值。
public class IntBufferDemo {
    public static void main(String[] args) {
        ByteBuffer bb = ByteBuffer.allocate(1024);
        IntBuffer ib = bb.asIntBuffer();
        ib.put(new int[]{11, 42, 47, 99, 143, 811, 1016});
        System.out.println(ib.get(3));
        System.out.println("-----");
        ib.put(3, 1811);
        // [11, 42, 47, 1811, 143, 811, 1016]
        ib.flip();
        while(ib.hasRemaining()) {
            int i = ib.get();
            System.out.println(i);
        }
    }
}
  • 上面代码中,先用重载后的put()方法存储一个整数数组。get()put()方法调用直接访问底层ByteBuffer中的某个整数位置。

小端:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
大端:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

  • ByteBuffer默认是大端形式存储数据,并且数据在网络中传送时也常常使用大端模式。可以使用带有参数ByteOrder.BIG_ENDIANByteOrder.LITTLE_ENDIANorder()方法改变ByteBuffer的字节排序方式。

缓冲器的细节

  • Buffer由数据和四个索引组成。四个索引markpositionlimitcapacity可以高效地访问及操纵Buffer中的数据。
方法 功能
capacity() 返回缓冲区的容量capacity
clear() 清空缓冲区,将position设置为0,limit设置为容量capacity,可以用于覆写缓冲区
flip() limit设为positionposition设为0,用于准备从缓冲区读取已经写入的数据
limit() 返回limit
limit(int lim) 设置limit
mark() mark设为position
position() 返回position
position(int pos) 设置position
remaining() 返回limit - position
hasRemaining() 若有介于positionlimit之间的元素,则返回true

内存映射文件

  • 内存映射文件可以创建和修改因为过大而不能放入内存的文件。对应类为MappedByteBuffer
public class LarggeMappedFiles {
    static int length = 0x8FFFFFFF;
    public static void main(String[] args) throws Exception {
        RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
        MappedByteBuffer mbb = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, length);
        for (int i = 0; i < length; i++)
            mbb.put((byte)'x');
        System.out.println("Finished writing");
        for (int i = length / 2; i < length / 2 + 6; i++)
            System.out.println((char)mbb.get(i));
    }
}

MappedByteBuffer主要是通过FileChannel#map方法,把文件映射到虚拟内存,并返回逻辑地址address,后续文件的读写操作都使用维护的虚拟内存地址和偏移进行操作。

文件加锁

  • 文件加锁机制允许同步访问某个作为共享资源的文件,并且文件锁对其它的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。
public class FileLocking {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("file.txt");
        FileLock fl = fos.getChannel().tryLock();
        if (fl != null) {
            System.out.println("Locked File");
            TimeUnit.MILLISECONDS.sleep(100);
            fl.release();
            System.out.println("Released Lock");
        }
        fos.close();
    }
}
  • 通过对FileChannel调用tryLock()lock,就可以获得整个文件的FileLocktryLock()是非阻塞式的,会尝试一次获得锁,如果失败则直接返回。lock()是阻塞式的,会阻塞进程直到获得锁,或调用lock()的线程中断,或调用lock()的通道关闭。Java虚拟机会自动释放锁或者关闭加锁的通道,也可以显式使用FileLock#release()释放锁。
  • 可以对文件的一部分上锁,使用重载的tryLock(long position, long size, boolean shared)lock(long position, long size, boolean shared),其中加锁区域由size - position决定,第三个参数指明是否为共享锁。
  • 对独占所或者共享锁的支持必须由底层的操作系统提供,如果操作系统不支持共享锁并为每一个请求都创建一个锁,那么就会使用独占锁。
  • SocketChannelDatagramChannelServerSocketChannel不需要加锁,因为这些通道是从单进程实体继承而来,通常不在两个进程之间共享网络socket。
  • 对于映射文件,同样可以进行部分加锁,以便其它进程可以修改文件中未被加锁的部分,例如数据库。其方法和上述过程相同。

Java编程思想笔记0x0d

Java I/O 系统(三)

标准I/O

  • 标准I/O是Unix中的概念,指程序所使用的单一信息流。程序的所有输入都可以来自标准输入,所有输出也都可以发送到标准输出,以及所有的错误信息都可以发送到标准错误。其意义在于,可以容易的吧程序传亮起来,一个程序的标准输出可以成为另一个程序的标准输入。

从标准输入中读取

  • Java提供了System.inSystem.outSystem.errSystem.outSystem.err已经事先被包装成了PrintStream对象,System.in是没有被包装过的InputStream
public class Test {
    public static void main(String[] args) throws IOException {
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = stdin.readLine()) != null && s.length() != 0)
            System.out.println(s);
    }
}

标准I/O重定向

  • Java的System类提供一些简单的静态方法调用,以允许我们队标准输入、输出和错误I/O流进行重定向。
public class Test {
    public static void main(String[] args) throws IOException {
        PrintStream console = System.out;
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("Test.java"));
        PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("test.out")));
        System.setIn(in);
        System.setOut(out);
        System.setErr(out);
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = br.readLine()) != null)
            System.out.println(s);
        out.close();
        System.setOut(console);
    }
}
  • 上面代码中将标准输入接在了文件上,而标准输出和标准错误重定向至另一个文件。此外,在开始部分保存了最初System.out的引用,在最后部分进行了还原。
  • I/O重定向操作的是字节流,而不是字符流,因此重定向时注意使用InputStreamOutputStream

进程控制

  • 可以使用Java执行控制台命令,并获取其标准输出、错误。
public class Test {
    public static void main(String[] args) throws IOException {
        OSExecute.command("ping 114.114.114.114");
    }
}
class OSExecute {
    public static void command(String command) {
        boolean err = false;
        try {
            Process process = new ProcessBuilder(command.split(" ")).start();
            BufferedReader results = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String s;
            while ((s = results.readLine()) != null)
                System.out.println(s);
            BufferedReader errors = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            while((s = errors.readLine()) != null) {
                System.err.println(s);
                err = true;
            }
        } catch (Exception e) {
            if(!command.startsWith("CMD /C")) 
                command("CMD /C" + command);
            else 
                throw new RuntimeException(e);
        }
        if (err) {
            System.err.println("Error executing " + command);
        }
    }
}

Java编程思想笔记0x0c

Java I/O 系统(二)

I/O流典型的使用方式

缓冲输入文件

class BufferedInputFile {
    public static String read(String filename) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(filename));
        String s;
        StringBuilder sb = new StringBuilder();
        while((s = in.readLine()) != null) sb.append(s + "\n");
        in.close();
        return sb.toString();
    }
}
  • 如果想要打开一个文件用于字符输入,可以使用以StringFile对象作为文件名的FileInputReader。为了提高速度,需要对文件进行缓冲,可以将所产生的引用传给一个BufferedReader构造器。BufferedReader也提供readLine()方法,可以进行内容读取。当readLine()返回null时即达到文件末尾。最后调用close()关闭文件。

从内存输入

public class Main {
    public static void main(String[] args) throws IOException {
        StringReader in = new StringReader("aaa bbb ccc ddd\n");
        int c;
        while((c = in.read()) != -1)
            System.out.print((char)c);
        in.close();
    }
}
  • 使用StringReader读取内存中的Stringread()每次会读取一个字节(是int形式,注意使用类型转换)。

格式化的内存输入

public class Main {
    public static void main(String[] args) throws IOException {
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("Main.java")));
        while (in.available() != 0) {
            System.out.println((char)in.readByte());
        }
        /* 一次读多个字节
        byte[] b = new byte[10];
        while (in.available() != 0) {
            in.read(b);
            System.out.println(Arrays.toString(b));
        }
        */
    }
}
  • 读取格式化的数据,需要使用DataInputStream,这是一个面向字节的I/O类。因为任何字节的值都是合法结果,所以需要available()方法检查还有多少可供存取的字符。

基本的文件输出

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader("a.txt"));
        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("b.txt")));
        int lineCount = 1;
        String s;
        while((s = in.readLine()) != null)
            out.println(lineCount++ + ": " + s);
        in.close();
        out.close();
    }
}
  • FileWriter对象可以向文件写入数据。首先,创建一个与指定文件连接的FileWriter。此外,通常会用BufferedWriter将其包装来缓冲输出,因为缓冲往往能够显著地提高I/O性能。为了提供格式化机制,使用了PrintWriter装饰。最后,应该显式调用Writer#close(),否则缓冲区内容不会刷新清空,内容也不会写入文件。
  • 在Java SE5中,PrintWriter添加了一个辅助构造器,可以省略其它装饰器,直接填入文件名String即可。

存储和恢复数据

public class Main {
    public static void main(String[] args) throws IOException {
        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("a.txt")));
        out.writeDouble(3.14159);
        out.writeUTF("That was pi");
        out.writeDouble(1.41413);
        out.writeUTF("Square root of 2");
        out.close();
        DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("a.txt")));
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
    }
}
  • 可以用DataOutputStreamDataInputStream以与机器无关方式从底层流中读写基本 Java 数据类型。
  • 当使用DataOutputStream写字符串并让DataInputStream能够恢复它的唯一可靠做法就是使用UTF-8编码,在上面代码中就是使用writeUTF()readUTF()
  • ASCII字符只占7位,为了避免空间浪费,UTF-8将ASCII字符编码成单一字节形式,而非ASCII字符则编码成两到三个字节的形式。另外,字符串的长度存储在UTF-8字符串的前两个字节。writeUTF()readUTF()使用的是UTF-8变体。
  • 为了保证所有的读方法能够正常工作,必须知道流中数据项所在的确切位置,要么为文件中的数据采用固定的割舍,标准要么将额外的信息保存在文件中以便能够对其解析来确定数据的存放位置。

Java编程思想笔记0x0b

Java I/O 系统(一)

File

  • File类既代表一个特定文件的名称,又能代表一个目录下的一组文件的名称。

    public class Main {
        public static void main(String[] args) {
            File path = new File(".");
            String[] list;
            if(args.length == 0)
                list = path.list();
            else 
                list = path.list(new DirFilter(args[0]));
            Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
            for(String dirItem: list)
                System.out.println(dirItem);
        }
    }
    
    class DirFilter implements FilenameFilter {
        private Pattern pattern;
        public DirFilter(String regex) {
            pattern = Pattern.compile(regex);
        }
        @Override
        public boolean accept(File dir, String name) {
            return pattern.matcher(name).matches();
        }
    }
    
    • 上面代码中,File#list()可以接受一个FilenameFilter对象,用于过滤文件。此处使用了回调,重载了accept()方法并让list()调用。

    输入和输出

    InputStream类型

    • InputStream的作用是用来表示那些从不同数据源产生的输入的类,包括:字节数组、String对象、文件、管道、由其他种类的流组成的序列等,每一种数据源都有相应的InputStream子类。
功能 构造器参数 如何使用
ByteArrayInputStream 允许将内存的缓冲区当做InputStream使用 缓冲区,字节将从中取出 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
StringBufferInputStream String转换成InputStream使用 字符串,底层是实现使用StringBuffer 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
FileInputStream 用于从文件中读取信息 字符串,表示文件名、文件或FileDescriptor对象 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
PipedInputStream 产生用于写入相关PipedOutputStream的数据,实现管道化的概念 PipedOutputStream 作为一种数据源:将其与FilterInputStream对象相连以提供有用接口
SequenceInputStream 将两个或多个InputStream对象转换成单一InputStream 两个InputSteam对象或者一个容纳InputStream对象的容器Enumeration 作为多线程中数据源:将其与FilterInputStream对象相连以提供有用接口
FilterInputStream 抽象类,作为装饰器的接口。其中,装饰器为其它的InputStream类提供有用的功能 - -

OutputStream类型

  • OutputStream类的类别决定了输出所要去往的目标:字节数组、文件或管道。
功能 构造器参数 如何使用
ByteArrayOutputStream 在内存中创建缓冲区,所有送往流的数据都要放置在此缓冲区。 缓冲区初始化尺寸(可选) 用于指定数据的目的地:将其与FilterOutputStream对象相连以提供有用接口
FileOutputStream 用于将信息写至文件 字符串,表示文件名、文件或FileDescriptor对象 用于指定数据的目的地:将其与FilterOutputStream对象相连以提供有用接口
PipedOutputStream 任何写入其中的信息都会自动作为相关PipedInputStream的输出。实现管道化的概念 PipedInputStream 用于指定多线程的数据的目的地:将其与FilterOutputStream对象相连以提供有用接口
FilterOutputStream 抽象类,作为装饰器的皆苦。其中,装饰器为其它OutputStream提供有用功能 - -

添加属性和有用的接口

FilterInputStream

功能 构造器参数 如何使用
DataInputStream DataOutputStream搭配使用,可以按照可移植方式从流读取基本数据类型(intcharlong等) InputStream 包含用于读取基本类型数据的全部接口
BufferedInputStream 可以防止每次读取时都得进行实际写操作,代表使用缓冲区。 InputStream,可选指定缓冲区大小 本质上不提供接口,只不过是向进程中添加缓冲区所必须的
LineNumberInputStream 跟踪输入流中的行号,可调用getLineNumber()setLineNumber(int) InputStream 仅增加了行号,因此可能要与接口对象搭配使用
PushbackInputStream 能弹出一个字节的缓冲区,因此可以将读到的最后一个字符回退。 InputStream 通常作为编译器的扫描器

FilterOutputStream

功能 构造器参数 如何使用
DataOutputStream DataInputStream搭配使用,可以按照可移植方式从流读取基本数据类型(intcharlong等) OutputStream 包含用于写入基本类型数据的全部接口
PrintStream 用于产生格式化输出。其中DataOutputStream处理数据的存储,PrintStream处理显示 OutputStream,可选是否每次换行时清空缓冲区 -
BufferedOutputStream 可以防止每次读取时都得进行实际写操作,代表使用缓冲区。 OutputStream,可选指定缓冲区大小 本质上不提供接口,只不过是向进程中添加缓冲区所必须的

Reader和Writer

  • InputStreamOutputStream是面向字节形式的I/O,而ReaderWriter则提供兼容Unicode与面向字符的I/O功能。

数据的来源与去处

InputStreamOutputStream ReaderWriter
InputStream Reader,适配器InputStreamReader
OutputStream Writer, 适配器OutputStreamWriter
FileInputStream FileReader
FileOutputStream FileWriter
- StringReader
- StringWriter
ByteArrayInputStream CharArrayReader
ByteArrayOutputStream CharArrayWriter
PipedInputStream PipedReader
PipedOutputStream PipedWriter

更改流的行为

FilterInputStreamFilterOutputStream FilterReaderFilterWriter
FilterInputStream FilterReader
FilterOutputStream FilterWriter
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
PrintStream PrintWriter
  • 如果需要使用readLine()方法,就不应该使用DataInputStream,而用BufferedReader代替。

DataInputStream#readLine()方法已被废弃,因为它无法正确地将字节转换为字符。


DNS与BIND笔记0x00

DNS的运行机制

域命名空间

  • DNS的本质是分布式数据库,通过域名来进行索引。每个域名本质上是一颗大型逆向树的一条路径,这棵逆向树就是域命名空间。

域名

  • 如果根节点(以”.”和一个空标签结束)出现在一个域名的结尾时,称之为完全限定域名(fully qualified domain name, FQDN),它能准确地标识出一个节点在层次结构中的位置。

  • 一个域就是域命名空间的一颗子树。

资源记录

  • 与域名相关的数据都被包含在资源记录(resource record, RR)中。记录按照所关联的网络或软件的类型被分成不同的类。

名称服务器和区域

  • 存储域命名空间信息的程序称作名称服务器(nameserver)。名称服务器通常只拥有域命名空间某一部分的完整信息,这一部分称作区域(zone)。加载区域后,名称服务器可宣称对此区域具有权威(authority)。
  • 区域和域的区别:所有的顶级域以及许多二级域和更低级别的域通过授权被划分成了更小更好管理的单元,这些单元就是区域。

名称服务器的类型

  • primary master:其区域数据来源于主机上的文件
  • slave(secondary master):其区域数据来源于primary master或者另外一台slave。

解析

解析器

  • 解析器是访问名称服务器,从域命名空间获取信息的客户端程序。

递归查询

  • 当名称服务器接收到递归查询请求时,如果它能够回答,就直接返回查询结果。如果它不能回答,那么它递归检查自己是否知道域名的权威名称服务器,如果知道,就给该权威名称服务器发送查询请求,如果不知道,则再检索上一级域名的权威名称服务器,直到检索根域名,此时将直接向根服务器发起查询请求。

迭代查询

  • 在迭代查询请求中,收到请求的名称服务器只会返回它认为的“最佳答案”,即返回解析结果或者它所知道的和域名最相近的权威名称服务器。

往返时间

  • 往返时间(roundtrip time,RTT)是同一区域的不同名称服务器的选择依据。

缓存

  • 名称服务器在处理递归查询时,会遇到许多与域命名空间有关的信息,它会记录下每次遇到的名称服务器、地址和对应的区域。这样既能加快处理查询请求的速度,也能减少向根服务器查询的次数。

生存时间

  • 生存时间(time to live,TTL)是名称服务器允许数据在缓存中存放的时间,如果到期,名称服务器就会丢弃过期数据,并从权威名称服务器获取新的数据。

Java编程思想笔记0x0a

容器深入研究

Collection的功能方法

方法 说明
boolean add(T) 添加元素,如果类型不为T则返回false。可选
boolean addAll(Collection<? extends T>) 添加所有元素,只要添加了元素就返回true。可选
void clear() 移除所有元素。可选
boolean contains(T) 如果容器包含该元素,则返回true
boolean containsAll(Collection<?>) 如果容器中包含所有元素,则返回true
boolean isEmpty() 容器中没有元素返回true
Iterator<T> iterator() 返回一个可以遍历容器所有元素的Iterator<T>
boolean remove(Object) 如果元素在容器中,则移除一个该元素的实例。如果发生了移除则返回true。可选
boolean removeAll(Collection<?>) 移除参数中所有的元素,如果发生了移除就返回true。可选
boolean retainAll(Collection<?>) 只保存参数中的元素,只要Collection发生了改变就返回true
int size() 返回容器中元素的数目
Object[] toArray() 返回包含容器中所有元素的数组
<T> T[] toArray(T[] a) 返回包含容器中所有元素的数组,数组类型与参数类型一致

可选操作

  • Collection方法中,一些方法是可选的,这意味着继承Collection时这些方法可以不进行覆盖。此时导出类对象调用该方法时会出现UnsupportedOperationExceptionCollection的可选方法实现如下:

    public boolean add(E e) {
        throw new UnsupportedOperationException();
    }
    

    可见,如果导出类不实现可选方法,那么该方法会调用Collection中的实现,即抛出一个UnsupportedOperationException异常。

Set与存储顺序

Set

  • 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法一确保对象的唯一性。SetCollection有完全一样的接口。Set不保证维护元素的次序。

HashSet

  • 为快速查找而设计的Set,存入HashSet的元素必须定义hashCode()HashSet存储元素的顺序是按照底层哈希桶号从小到大排序,如果桶号相同就按照放入桶的先后顺序。

TreeSet

  • 保持次序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口

LinkedHashSet

  • 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的顺序)。

SortedSet

  • 可以保证其中的元素处于排序状态,并提供一些附加功能

队列

LinkedList

  • 队列的基本实现。

优先级队列

  • PriorityQueue:按照元素从小到大排列。

Map

Map的各种实现

  • HashMapMap基于散列表的实现。插入和查询键值对的开销是固定的,可以通过构造器设置容量和负载因子,以调整容器性能

负载因子:如果容器持有元素超过一定比例会进行扩容,此时作为底层实现的旧数组会被更大容量的新数组取代。这个比例就是负载因子。

  • LinkedHashMap:类似于HashMap,但是迭代遍历时,取得键值对的顺序是插入顺序,或者是最近最少使用(LRU)次序。仅比HashMap慢一点,而在迭代访问时反而更快,因为其使用链表维护内部次序。
  • TreeMap:基于红黑树的实现。查看键或者键值对时,它们会被排序。TreeMap的特点在于,所得到的结果是经过排序的(按元素大小排序)。TreeMap是唯一带有subMap()方法的Map,它可以返回一棵子树。
  • WeakHashMap:弱键映射,允许释放映射所指向的对象,这是为解决某类特殊问题而设计的。如果映射之外没有引用指向某个键,则此键可以被垃圾回收。
  • ConcurrentHashMap:一种线程安全的Map,它不涉及同步加锁。
  • IdentityHashMap:使用==代替equals()对键进行比较的散列映射,专为解决特殊问题而设计的。

散列

  • HashMap使用了散列码作为键的搜索。在Java中,hashCode()是根类Object的方法,因此所有Java对象都能产生散列码。使用hashCode()进行查询能够显著地提高性能。
  • 默认的Object#equals()只是比较对象地址,如果需要某个自定义的类作为键使用,需要重载hashCode()equals()

容器的选择

List

  • ArrayList应该作为默认选择,只有当程序的性能因为经常从表中间进行插入和删除而变差的时候,才去选择LinkedList。如果使用的是固定数量的元素,也可以直接使用数组。

Set

  • HashSet的性能基本上总是比TreeSet好,特别是在添加和查询元素时。当需要对元素进行排序,或者需要对元素进行迭代时才会需要TreeSet

Map

  • 类似于Set的结果,TreeMapHashMap要慢。

实用方法

方法 说明
max(Collection), min(Collection) 返回参数Collection中最大或者最小的元素,采用Collection中内置的自然比较法
max(Collection, Comparator), min(Collection, Comparator) 返回参数Collection中最大或最小的元素,采用参数中的Comparator进行比较
indexOfSubList(List source, List target) 返回targetsource中第一次出现的位置,或者在找不到时返回-1
lastIndexOfSubList(List source, List target) 返回targetsource中最后一次出现的位置,或者在找不到时返回-1
replaceAll(List<T>, T oldVal, T newVal) 使用newVal替换所有oldVal
reverse(List) 逆转所有元素的次序
reverseOrder(), reverseOrder(Comparator<T>) 返回一个Comparator,它可以以逆转实现了Comparator<T>的对象集合的自然顺序。第二个版本可以逆转所提供的Comparator的顺序
rotate(List, int distance) 所有元素向后移动distance个位置,将末尾的元素循环到前面来
shuffle(List), shuffle(List, Random) 随机改变指定列表顺序。第一种形式提供其自己的随机机制,第二种则来源于参数Random对象
sort(List<T>), sort(List<T>, Comparator<? super T> c) 使用List<T>中的自然顺序排序。第二种形式允许提供用于排序的Comparator
copy(List<? super T> dest, List<? extends T> src) src中的元素复制到dest
swap(List, int i, int j) 交换list中位置i与位置j的元素,通常比手动实现要快
fill(LIst<? super T>, T x) 用对象x替换list中的所有元素
disjoint(Collection, Collection) 当两个集合没有重复元素时返回true
frequency(Collection, Object x) 返回Collectionx出现的次数
emptyList(), emptyMap(), emptySet() 返回不可变且为泛型的ListMapSet
singleton(T x), singletonList(T x), singletonMap(K key, V value) 产生不可变的Set<T>List<T>Map<K, V>
  • 在比较字符串而使用max()min()sort()等方法时,使用参数String.CASE_INSENSITIVE_ORDER可以忽略大小写。

同步控制

  • Collections.synchonizedCollection()可以传入一个新生成的容器,返回的是有同步功能的版本。类似的还有Collections.synchonizedList()Collections.synchonizedSet()Collections.synchonizedMap()等。
  • 快速报错:Java容器的一种保护机制,能够防止多个进程同时修改同一个容器的内容。它会探查容器上的任何除了当前进程进行的操作以外的所有变化,一旦发现其它进行修改了容器,就立刻抛出ConcurrentModificationException异常。

Java编程思想笔记0x09

泛型(四)

动态类型安全

  • 因为可以向旧版本的Java代码中传递容器,而如果旧版本的代码没有使用泛型则需要动态类型检查。java.uitl.Collections中有一系列方法可以完成这项工作:checkedCollection()checkedList()checkedMap()checkedSet()checkedSortedMap()checkedSortedSet()。其参数为希望获得检查的容器和强制要求的类型。受检查的容器在插入不正确的类型时会抛出ClassCastException。例如:

    List<Cat> cats = Collections.checkedList(new ArrayList<>(), Cat.class);

    此时插入Cat对象是正确的,而插入Dog对象则会出现异常。

异常

  • 由于擦除的原因,将泛型应用于异常是非常受限的。catch语句不能捕获泛型类型异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承Throwable
  • 在方法的throw子句可以使用类型参数,使得随检查异常的类型发生变化而变化的代码

混型

  • 混型是指回合多个类的能力,以产生一个可以表示混型中所有类型的类。
  • 混型的价值之一是可以将特性和行为一致地应用于多个类型,如果在混型类中修改,那么这些修改将会应用于混型所应用的所有类型之上。混型有一点面向切面编程的意思。

与接口混合

  • 一种常见的混型方法就是使用接口产生混型效果,即使用代理,每个混入类型都有一个相应的域,调用方法时要转发给对应的域。

使用装饰器模式

  • 装饰器是通过使用组合和形式化结构(可装饰物/装饰器层器结构)类实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当做一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。(即声明一个混型,其泛型类型参数为混型基于的所有类型,以达到混型的目的

与动态代理混合

  • 可以使用动态代理实现更为接近混合模型的机制。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。
  • 由于动态代理的限制,每个被混入的类都必须是某个接口的实现。
  • 在调用混入类型的方法之前,需对混型对象进行向下转型到合适的类型。

潜在类型机制

  • 潜在类型机制,又称鸭子类型机制,具有这种机制的语言只要求实现某个方法的自己,而不是某个特定类型或者接口,从而放松了限制。潜在类型机制可以横跨类型继承结构,调用某个不是公共接口的方法。
  • 在Java中是没有原生实现的潜在类型机制,会被强制要求继承某个类或者实现某个接口。

对缺乏潜在类型机制的补救

反射

class CommunicateReflectively {
    public static void perform(Object speaker) {
        Class<?> spkr = speaker.getClass();
        try {
            Method speak = spkr.getMethod("speak");
            speak.invoke(speaker);
        } catch(NoSuchMethodException e) {
            System.out.println(speaker + "cannot speak");
        }
    }
}

上面的代码使用了反射获取了指定名称的方法,但是类型检查转移到了运行时。

将一个方法应用于序列

public class Apply {
    public static <T, S extends Iterable<? extends T>> 
        void apply(S seq, Method f, Object... args) {
        try {
            for (T t: seq) {
                f.invoke(t, args);
            }
        } catch(Exception e) {
            throw new RuntimException(e);
        }
    }
}
  • 上面代码的目的是实现对任意的序列S<T>调用某种方法,然而恰好序列都是实现了Iterable接口的。但是这种方法并不适用于没有实现内建接口或者继承内建类的类型。

用适配器模式仿真潜在类型机制

  • 可以使用适配器模式来适配已有的接口,来产生需要的接口。

数组

数组的特点

  • 效率:数组是一种效率最高的存储和随机访问对象引用序列的方式,代价是数组对象的大小被固定,并且在其生命周期中不能改变。
  • 类型:相比缺少泛型的容器,数组可以持有某种类型的对象,可以通过编译期检查来防止不当的操作。
  • 持有基本类型:数组可以持有基本类型,但是容器不能直接持有基本类型。

Java编程思想笔记0x08

泛型(三)

问题

任何基本类型都不能作为类型参数

  • Java泛型中不能使用基本类型用作类型参数,取而代之可以使用基本类型的包装器以及自动包装机制。

实现参数化接口

  • 一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。

转型和警告

  • 使用带有泛型类型参数的转型或instanceof不会有任何效果。因为类型参数T会被擦除到第一个边界,默认为Object,使用转型或instanceof也只使用了Object
  • 在必须进行转型到某种泛型类时,需要使用泛型类对象转型,例如List.class.cast(x),但是无法转换到具体类型,即不能使用List<RealClass>.class.cast(x)

重载

  • 由于擦除的原因,相同泛型类、不同类型参数的重载方法将产生相同的类型签名。

基类劫持接口

  • 当父类实现泛型接口时,实现了接口中的某个带有相应泛型参数的方法,此时子类将不能修改该方法的参数类型。

自限定的类型

古怪的循环泛型

  • 诸如class Sub extends Basic<Sub>的定义就是古怪的循环泛型,子类类型出现在了基类中。
  • 其本质是基类用导出类替代其参数,意味着泛型基类变成了一种其所有导出类的公共功能的模板,但是这些功能对于其所有的参数和返回值,将使用导出类型。

自限定

  • 下面代码中SelfBounded即自限定的泛型基类。
class SelfBounded<T extends SelfBounded<T>> {
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() {
        return element;
    }
}

继承SelfBounded必须类似与class A extends SelfBounded<A>来定义子类,这保证了类型参数必定与正在被定义的类相同。

同样的,自限定还可以用于泛型方法,防止该方法用于自限定参数之外的任何事物上:

public class SelfBoundingMethods {
    static <T extends SelfBoundingMethods<T>> T f (T arg) {
        return arg.set(arg).get();
    }
    public static void main(String[] args) {
        A a = f(new A());
    }
}

参数协变

  • 自限定类型的意义在于能够产生协变参数类型,即方法参数类型会随着子类变化而变化,不会出现重载。

但实际上如果仅为了实现参数协变,自限定并不是必要的,使用循环泛型就能解决,例如:

public class Test {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.set(a));
        System.out.println(a);
        System.out.println(a.get());
        B b = new B();
        System.out.println(b.set(b));
        System.out.println(b);
        System.out.println(b.get());
    }
}
class SelfBounded<T extends SelfBounded<T>> {
    T element;
    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() {
        return element;
    }
}
class Cyclic<T> {
    T element;
    Cyclic<T> set(T arg) {
        element = arg;
        return this;
    }
    T get() {
        return element;
    }
}
class A extends SelfBounded<A> {}
class B extends Cyclic<B> {}
/* Output:
A@2c13da15
A@2c13da15
A@2c13da15
B@9e89d68
B@9e89d68
B@9e89d68
*/

上面代码中,SelfBounded是自限定的,而Cyclic仅是普通的泛型类,其子类B使用了循环泛型就实现了参数协变。两者唯一的不同仅在于自限定中类型参数必须是自限定的,而循环泛型并无此限制