使用 final 成员处理构造函数中捕获的 Java 异常

Posted

技术标签:

【中文标题】使用 final 成员处理构造函数中捕获的 Java 异常【英文标题】:Handling Java exceptions caught in constructors, with final members 【发布时间】:2015-11-11 04:22:27 【问题描述】:

我有一些丑陋的代码,想重构它:

public class UdpTransport extends AbstractLayer<byte[]> 
    private final DatagramSocket socket;
    private final InetAddress address;
    private final int port;
    /* boolean dead is provided by superclass */

    public UdpTransport(String host, int port) 
        this.port = port;
        InetAddress tmp_address = null;
        try 
            tmp_address = InetAddress.getByName(host);
         catch (UnknownHostException e) 
            e.printStackTrace();
            dead = true;
            socket = null;
            address = null;
            return;
        
        address = tmp_address;
        DatagramSocket tmp_socket = null;
        try 
            tmp_socket = new DatagramSocket();
         catch (SocketException e) 
            e.printStackTrace();
            dead = true;
            socket = null;
            return;
        
        socket = tmp_socket;
    
    ...

导致丑陋的问题是final 成员之间的交互和捕获的异常。如果可能的话,我想保留成员final

我想按如下方式编写代码,但 Java 编译器无法分析控制流 - 不可能再次分配 address,因为第一次尝试的分配必须抛出以获得控制权到达catch 子句。

public UdpTransport(String host, int port) 
    this.port = port;
    try 
        address = InetAddress.getByName(host);
     catch (UnknownHostException e) 
        e.printStackTrace();
        dead = true;
        address = null; // can only have reached here if exception was thrown 
        socket = null;
        return;
    
    ...

Error:(27, 13) error: variable address might already have been assigned

有什么建议吗?

附:我有一个约束,即构造函数不会抛出 - 否则这一切都很容易。

【问题讨论】:

有没有想过可以使用非final而不是使用final实例变量?如果您想确保类外的不变性,请在 getter 中返回防御性副本。 socket创建失败是否需要address == null 为什么你不能有另一个tmp_address并在最后分配address = tmp_address 构造函数通常不应捕获异常。如果无法正确构造对象,则对应用程序没有任何用处。 从实践的角度来看,这与构造函数无关。如果您无法处理异常并修复它,那么让它冒泡。当然不要吞下它并打印堆栈跟踪。 【参考方案1】:

让构造函数抛出异常。如果构造函数没有正常终止,则保留 final 未赋值是可以的,因为在这种情况下没有返回任何对象。

代码中最丑陋的部分是在构造函数中捕获异常,然后返回现有但无效的实例。

【讨论】:

谢谢,我会认真考虑的。 如果我这样做 new Layer0(new Layer1(new Layer2(params...), params...), params...) 整个堆栈将失败,Layer1 将不知道它应该使用哪种类和构造函数参数来构建新的 Layer2。 再想一想,根本原因是我需要为协议栈中的每一层提供一种构造其子层的方法,而不是给它一个已经构造好的子层。这很棘手......【参考方案2】:

如果您可以随意使用私有构造函数,则可以将构造函数隐藏在公共静态工厂方法后面,该方法可以返回您的 UdpTransport 类的不同实例。比方说:

public final class UdpTransport
        extends AbstractLayer<byte[]> 

    private final DatagramSocket socket;
    private final InetAddress address;
    private final int port;
    /* boolean dead is provided by superclass */

    private UdpTransport(final boolean dead, final DatagramSocket socket, final InetAddress address, final int port) 
        super(dead);
        this.socket = socket;
        this.address = address;
        this.port = port;
    

    public static UdpTransport createUdpTransport(final String host, final int port) 
        try 
            return new UdpTransport(false, new DatagramSocket(), getByName(host), port);
         catch ( final SocketException | UnknownHostException ex ) 
            ex.printStackTrace();
            return new UdpTransport(true, null, null, port);
        
    


上面的解决方案可能有以下注意事项:

它只有一个构造函数,只将参数分配给字段。因此,您可以轻松拥有final 字段。这与我记得在 C# 和可能是 Scala 中称为 primary constructors 的内容非常相似。 静态工厂方法隐藏了UdpTransport 实例化的复杂性。 为简单起见,静态工厂方法返回一个实例,其中socketaddress 设置为真实实例,或者它们设置为null 指示无效状态。因此,此实例状态可能与您问题中的代码生成的状态略有不同。 此类工厂方法允许您隐藏它们返回的实例的真正实现,因此以下声明完全有效:public static AbstractLayer&lt;byte[]&gt; createUdpTransport(final String host, final int port)(注意返回类型)。这种方法的强大之处在于,如果可能的话,您可以根据需要将返回值替换为任何子类,除非您使用 UdpTransport 特定的公共接口。 另外,如果你对无效的状态对象没问题,我猜,那么无效的状态实例不应该包含一个真正的端口值,允许你进行以下操作(假设-1 可以指示一个无效的端口值,甚至可以为空Integer 如果您可以随意更改类的字段并且原始包装器对您没有限制):
private static final AbstractLayer<byte[]> deadUdpTransport = new UdpTransport(true, null, null, -1);
...
public static AbstractLayer<byte[]> createUdpTransport(final String host, final int port) 
    try 
        return new UdpTransport(false, new DatagramSocket(), getByName(host), port);
     catch ( final SocketException | UnknownHostException ex ) 
        ex.printStackTrace();
        return deadUdpTransport; // it's safe unless UdpTransport is immutable
    
最后,恕我直言,以这种方法打印堆栈跟踪不是一个好主意。

【讨论】:

【参考方案3】:

构造函数的两个版本,您的属性保持最终状态。请注意,我不认为您的原始方法通常“丑陋”。不过可以改进它,因为两个异常的 try-catch-block 是相同的。这成为我的构造函数 #1。

public UdpTransport(String host, int port) 
    InetAddress byName;
    DatagramSocket datagramSocket;

    try 
        byName = InetAddress.getByName(host);
        datagramSocket = new DatagramSocket(); 
     catch (UnknownHostException | SocketException e) 
        e.printStackTrace();
        dead = true;
        datagramSocket = null;
        byName = null;
    
    this.port = port;
    this.socket = datagramSocket;
    this.address = byName;


public UdpTransport(int port, String host) 

    this.port = port;
    this.socket = createSocket();
    this.address = findAddress(host);


private DatagramSocket createSocket() 
    DatagramSocket result;
    try 
        result = new DatagramSocket(); 
     catch (SocketException e) 
        e.printStackTrace();
        this.dead = true;
        result = null;
    
    return result;


private InetAddress findAddress(String host) 
    InetAddress result;
    try 
        result = InetAddress.getByName(host);
     catch (UnknownHostException e) 
        e.printStackTrace();
        this.dead = true;
        result = null;
    
    return result;

【讨论】:

【参考方案4】:

这个构造函数没有任何正当理由去捕捉异常。一个充满null 值的对象对应用程序毫无用处。构造函数应该抛出那个异常。抓不住。

【讨论】:

【参考方案5】:

这样的事情怎么样:

public class UdpTransport extends AbstractLayer<byte[]> 
    private final DatagramSocket socket;
    private final InetAddress address;
    private final int port;

    public static UdpTransport create(String host, int port) 
        InetAddress address = null;
        DatagramSocket socket = null;
        try 
            address = InetAddress.getByName(host);
            socket = new DatagramSocket();
         catch (UnknownHostException | SocketException e) 
            e.printStackTrace();
        
        return new UdpTransport(socket, address, port);
    

    private UdpTransport(DatagramSocket socket, InetAddress address, int port) 
        this.port = port;
        this.address = address;
        this.socket = socket;
        if(address == null || socket == null) 
           dead = true;
        
    
    ...

【讨论】:

以上是关于使用 final 成员处理构造函数中捕获的 Java 异常的主要内容,如果未能解决你的问题,请参考以下文章

Java内存模型

Java final原理

[JMM]__JMM中的普通final域重排序规则

[JMM]__JMM中引用类型final域重排序规则

final与4个修饰成员的关键字

第10课 面向对象的增强(default/deleteoverride/final)