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 {

        AddC 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()也由对象内存地址决定(不是内存地址)。为了保证在Hash表中,同一个对象只被保留一份,在重写了equals后必须重写hashCode,而可以使用equals()判断唯一性何又还要hashCode呢,答案是保证查询的效率,如果用equals()一个一个比较,复杂度达到O(N)。

// 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

以上是对象的内存地址以及hashCode默认值。

// Employee.java
public class Employee {
    int id;
    public int getId() {
        return id;
    }
    public void setId(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();
        Employee b = new Employee();
        a.setId(1);
        b.setId(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);
        System.out.println(a.hashCode()+","+b.hashCode());
    }
}

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

从上面的例子中可以看到,虽然hashCode相等,但是两个对象本身不相同,根据id%10的规则,同一个hashCode可能对应于多个对象,equals()为true的对象才真正是同一个对象。实际情况下,我们需要重写equals,如上例中同一id的员工本身为同一对象,此时也必须重写hashCode,才能保证同一个对象在hash表中对应唯一的hashCode。总地来说就是相同对象对应相同的hashCode,但同一个hashCode可能对应于多个对象,如采用id%10的策略。

在HashSet.add(b)时,先查找是否有对应的hashCode,如果有,该hashCode可能对应一个对象列表,再使用equals()比较同一对象,如果有同一对象存在,则不执行add操作。使用hashCode能显著提高查询效率,从O(N)降到O(1)。

因此,对于Employee,应该如下重写equals()和hashCode(),Ideaj和Eclipse都可以快速帮助我们生成。

public class Employee {
    int id;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HashTest hashTest = (HashTest) o;
        if (id != hashTest.id) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

本文参考:

Java深度历险(七)——Java反射与动态代理
Java反射–系列教程——make_dream【译】
Java中hashCode的作用——冯立彬