JAVA进阶汇集

本文整理JAVA中的进阶知识,力图以最简洁的方式理解JAVA的博大精深。

远程方法调用–RMI

Remote Methed Invocation 是分布式架构的基础。

以下实现一个简单的分布式小程序——计算两个数的和。

  1. 编写接口 AddI.java
  2. 实现接口(远程对象) AddC.java
  3. 服务端 Server.java
  4. 客户端 Client.java

其中接口是整个程序的指导性文件,其定义了分布式程序共有的方法,包括方法名、参数和返回值。 服务端实现接口的具体内容,并开启监听,对客户端提供服务。 客户端根据接口文件提供的方法,进行调用。接口是二者调用传送数据的凭证。

为了代码的简洁,我未给出包的引用,编译器可以自动提示添加即可,或者自己添加java.rmi.*java.rmi.Registry.*

定义接口,接口要继承自Remote才支持远程调用,方法必须进行异常处理

// AddI.java
public interface AddI extends Remote {
    public int add(int x, int y) throws RemoteException;
}

实现接口,即远程对象,需要继承自UnicastRemoteObject

// AddC.java
public class AddC extends UnicastRemoteObject implements AddI {
    public AddC() throws Exception {
        super();
    }
    
    @Override
    public int add(int x, int y) throws RemoteException {
        return x + y;
    }
}

服务端绑定端口,提供调用服务

// Server.java
public class Server {
    public static void main(String args[]) throws Exception {

        AddI addService = new AddC();         // 对外提供服务的对象
        LocateRegistry.createRegistry(1099);  // 注册监听
        Naming.rebind("ADD_SEV", addService);  // 绑定唯一服务名称,服务对象
        System.out.println("server started");
    }
}

客户端根据AddI接口调用方法

// Client.java
public class Client {
    public static void main(String args[]) throws Exception {
        // 获取到远程对象,需要提供服务的ip和port,以及服务唯一标识
        AddI addService = (AddI) Naming.lookup("rmi://127.0.0.1:1099/ADD_SEV");
        int sum = addService.add(4,5);    // 调用远程对象
        System.out.println("sum = "+sum);
    }
}

编译与调用:

服务端 服务端 再开一个终端 客户端

问题: 服务端和客户端在不同机器上时,上面运行可能出现 java.rmi.ConnectException: Connection refused to host: 127.0.1.1 或者 java.rmi.ConnectException: Connection refused to host: 127.0.0.1 ,这都是服务端引起的,服务端所在的主机hostname对应的ip与客户端连接它使用的ip不对应。我们可以在服务端通过hostname -i查看对应的ip,rmi机制中,当客户端请求连接服务端后,服务端要返回hostname对应ip确认,而此时hostname对应ip与客户端请求不符合,将造成上面的错误,因此解决方法是:修改 /etc/hosts ,将主机名对应的ip修改为真实ip。

# /etc/hosts
192.168.1.24    Ubuntu # Ubuntu是我的主机名

反射–Reflection

// MyClass.java
class MyClass {
    public int count;
    public MyClass(int count) {
        this.count = count;
    }
    public void increase(int step) {
        count = count + step;
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        // 一般调用方法
        MyClass myClass = new MyClass(5); 
        myClass.increase(2);
        System.out.println("Normal -> " + myClass.count);
    }
}

上面是正常使用构造函数定义对象,调用对象方法,以及获取对象的成员变量的方法。以下,使用反射可以达到同样的效果。

// 使用反射方法
try {
    Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法
    MyClass myClassReflect = (MyClass) constructor.newInstance(5); //创建对象

    Method method = MyClass.class.getMethod("increase", int.class);  //获取方法
    method.invoke(myClassReflect, 2); //调用方法

    Field field = MyClass.class.getField("count"); //获取域
    System.out.println("Reflect -> " + field.getInt(myClassReflect)); //获取域的值
} catch (Exception e) {
    e.printStackTrace();
}

在上面的例子中,使用MyClass.class.getConstructor(int.class)来获取本来为public MyClass(int count)的构造方法。必须保证参数对应,否则会报出IllegalArgumentException的异常,getConstructor使用参数来区分对应的构造函数。有了构造函数,就可以通过constructor.newInstance(5)来定义对象了。使用同样的方法,我们可以获取到MyClass.class的方法和成员对象。

使用反射应该建立一种思想,先建立一个和目标对应的反射对象,然后使用反射对象。

核心方法:

// 创建对象
constructor = class.getConstructor(参数)
reflection = constructor.newInstance(参数)
// 执行方法
method = getMethod(方法名,参数)
method.invoke(参数)
// 获取成员值
field = getField(成员名)
field.getInt(reflection)
// 获得所有成员值
getFields()

但是,使用上面的几种方法只能访问public的方法、变量。需要访问非公有方法需要使用对应的getDeclaredXXX()版本,使用方法一样。而对于private修饰的变量,通过field.getXX()获取值时会出现权限不足的IllegalAccessException异常,此时需要在调用field.getXX()之前调用field.setAccessible(true),才能正常访问。

重要方法:

getAnnocations()  // 返回注解,在注解运用中至关重要!

下面给出一张反射的方法汇总

点击查看

动态代理–Proxy

Java 动态代理指创建一个代理对象来接管原对象的方法,以达到灵活控制的目的。 其原理是使用JAVA反射得到对象的方法,以便在其方法执行前后添加自定义代码。

如下是一个禁止List使用add方法的例子,返回一个代理的List接管了之前的List:

public class ProxyTest {
    // 禁止List的add方法
    public static List getList(List list) {
        return (List) Proxy.newProxyInstance(ProxyTest.class.getClassLoader(), new Class[]{List.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("add")){
                    throw new UnsupportedOperationException();
                }else {
                    return method.invoke(proxy,args);
                }
            }
        });
    }

    public static void main(String args[]){

        List<String> list = new ArrayList<String>();
        list.add("hello");  // 正常

        list = getList(list);
        list.add("world");  // 抛出异常
    }
}

使用动态代理只支持接口对象。

创建代理对象的方法是

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler);

因此,代理对象可以实现多个接口,即第二个列表中的所有接口。

InvocationHandler是方法调用程序处理器。

范型–GenericType

Java范型广泛应用于容器类,如ArrayList<String>即是一个使用泛化的例子,内部定义为ArrayList<T>。 当然,大多数时候,范型是可以用Object代替的,相比Object,使用范型更加安全,编译器能对类型的检查更加完善。使用范型不需要使用cast,能在编译时就发现问题。

范型的简单例子:

// GenericType.java
public class GenericType<T> {
    private T t;
    public GenericType(T t){
        this.t = t;
    }
    public T getValue(){
        return t;
    }
}

// Test.java
public class Test {
    public static void main(String... args) {
        GenericType<String> gts = new GenericType<String>("Hello");
        String value = gts.getValue();
        
        GenericType<Integer> gti = new GenericType<Integer>(1);
        int number = gti.getValue();
    }
}

上面是一个基本的例子,如果换用Object来实现,需要cast,并且可能发生ClassCastException异常。 同时,使用范型接口,能对实现类直接生成具体的类,下面是一个例子:

// GenericTypeInterface.java
public interface GenericTypeInterface<T> {
    public T fun(T t);
}

// GenericTypeImpl.java
public class GenericTypeImpl implements GenericTypeInterface<String> {
    @Override
    public String fun(String s) {  // 能生成具体参数和返回值类型
        return null;
    }
}

范型可以用来限制某些类才能合法使用方法: List<? extends Fruit> 表示 List 中可以放置 Fruit 的子类, 如ArrayList<Apple>ArrayList<Orange> List<? super Fruit> 表示 List 中可以放置任意 Fruit 的父类, 如ArrayList<Object>

NIO

Buffer

四个变量:capacity,position,limit,mask
四个操作:clear,flip,rewind,compact

flip()是为了读缓冲区做准备
clear()和compact()
是为写缓冲区做准备,区别是compact只会删除已经读取的部分,未读的部分被复制到缓冲区开始位置

创建一个存储48字节(byte)的buffer
ByteBuffer buffer = ByteBuffer.allocate(48)
创建一个存储48字符(char)的buffer
CharBuffer buffer = CharBuffer.allocate(48)

equals()和compareTo()方法比较的是剩余元素(即position到limit之间的元素)

channel.read(ByteBuffer[]) 前面的buffer读满才会读取到后面的buffer
channel.write(ByteBuffer[]) 只依次写入各个buffer中position到limit之间的数据

channel.transferFrom(position, count, fromChannel) 中的count指最多读取数,实际读取数可能小于此值。
channel.transferTo(position, count, toChannel) 与上类似。

Selector

Selector能够同时监管多个Channel,因此能够使用单线程替代多线程,减小开销。
原理:Channel注册自己的“interest”到Selector,告诉Selector我自己的状态变为interest时发出通知。
举例:在使用Socket时,Server上需要监听,等待客户端连接,双方也需要等待对方发送数据,直到可读可写。在不是使用Selector时,Server上通常需要多个线程监听等待,而使用Selector可以解决这一问题。

Selector selector = Selector.open();
serverSocketChannel.regist(selector, SelectionKey.OP_ACCEPT); // 当用客户端accept时通知我
socketChannel.regist(selector, SelectionKey.OP_READ);         // 当状态变为可read时通知我
while(true) {
    int readyChannels = selector.select(); // selector阻塞直到有至少一个channel状态达到通知状态返回
    if (readyChannels == 0) continue;      // 超时,仍然没有channel准备好,返回0
    selector.selectedKeys();               // 返回ready的通道
    ... // 对相关channel进行处理
}

如上,通过Selector.select()的阻塞,便可以服务大众,替代多个线程。

select()只对channels对应“interest”的事件发生时才返回,对其不interest的事件即使发生也不返回,返回值代表channel ready事件的个数。

如果不显式的通过 key.remove() 移除对应的 key, 下一次依然会通知。

hashCode 与 equals

二者都是Object默认的方法,默认情况下,equals()比较两个对象的内存地址是否相等。hashCode()用于Hash表快速查找对象,hashCode()也由对象内存地址决定(值不是内存地址)。以下程序查看对象的内存地址以及hashCode。

// HashTest.java
public class HashTest {
}

// Test.java
public class Test {
    public static void main(String[] args) {
        HashTest a = new HashTest();
        HashTest b = new HashTest();
        System.out.println(a+", "+b);
        System.out.println(a.hashCode()+", "+b.hashCode());
    }
}

// 输出
HashTest@1540e19d, HashTest@677327b6
356573597, 1735600054

equals 用于判断对象是否相同,那 hashCode 的作用是什么?

hashCode 主要用于 Hash 表,我们知道 Hash 表查询速度很快,其采用“数组+链表”的存储方式,通过特定的哈希函数,将所要存储的数据映射到对应的地址,下次访问直接计算得到内存地址取值,当多个存储对象出现哈希冲突时,采用拉链法,从该位置建立链表,当采用的哈希函数适当时,数据能够均匀分布,查询速度接近O(1)。

不难看出,hashCode 用于第一步寻址,直接找到目标或链表头结点,而 equals 顺着链表找到目标。

重写 hashCode 与 equals

为什么要重写?因为很多时候我们会遇到这种场景:

// Person.java
public class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        HashMap<Person, Integer> ps = new HashMap<Person, Integer>();
        ps.put(new Person("wang"), 100);
        System.out.println(ps.get(new Person("wang")));
    }
}

// 输出
null

分析一下,为什么输出是 null, 因为通过new 创建的新对象与 HashMap 中的对象 hashCode() 不一致,equals() 更不一致,在第一步查找定位就失败了。

因此,我们必须重写上述两个方法, 重写的代码很繁琐,所幸 Ideaj 和 Eclipse 都可以自动快速帮助我们生成。Idea 下通过 Alt + Insert 可以调出自动生成菜单。

// Person.java
public class Person {
    String name;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return !(name != null ? !name.equals(person.name) : person.name != null);
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

这里的 hashCode() 默认使用了字符串的哈希值计算方法,哈希函数影响查询的效率,我们也能自定义哈希函数,例如经典的取模法,以下是一个取模法的例子。

// Employee.java
public class Employee {
    int id;
    public Employee(int id) {
        this.id = id;
    }
    @Override
    public int hashCode() {
        return id % 10;
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        Employee a = new Employee(1);
        Employee b = new Employee(1);
        Set<HashTest> set = new HashSet<HashTest>();
        set.add(a);
        set.add(b);
      	System.out.println(a.hashCode()+", "+b.hashCode());
        System.out.println(a.equals(b));
        System.out.println(set);
    }
}

// 输出
1, 1
false
[HashTest@1, HashTest@1]

在HashSet.add(b)时,先查找是否有对应的hashCode,再使用equals()比较对象,如果有同一对象存在,则不执行add操作。由于没有重写 equals() 方法,HashSet 添加了两个对象,其 hashCode 均为 1。

总结

hashCode() 和 equals() 是哈希表存储和查询对象的重要依据,两者必须同时重写。

不同对象 equals() == false,但 hashCode 可以相同。

本文参考:

Java深度历险(七)——Java反射与动态代理
Java反射–系列教程——make_dream【译】
Java中hashCode的作用——冯立彬 HashMap实现原理及源码分析