Java高级系列——如何高效的编写方法(methods)

一、介绍

本文我们将会花一些时间从不同的方面去讨论一下Java中方法的设计和实现方式。在本系列前面的文章中我们已经看到,在Java中编写一个方法非常的简单,但是在编写方法时掌握一些关键的要素可以让方法可读性更强而且更加高效。

二、方法签名(Method signatures)

我们已经知道,Java是面向对象语言。因此,Java中的每个方法属于类的实例(static方法属于类本身),每个方法都有可见(或者可访问)规则,方法也可能被定义成abstract或者final类型等。然而,在方法中要讨论的最重要的一部分就是方法的签名、返回类型和参数、再加上实现方法中可能抛出的一系列的异常。首先我们从一个小的例子说起:

public static void main(String[] args) {
    // Some implementation here
}

main方法接受一个字符串数组作为参数并且什么都不返回。如果所有的方法都像main方法一样简单是非常nice的,但是实际上,main方法的方法签名实际上可读性很差。让我们看一下下面的例子:

public void setTitleVisible(int lenght, String title, boolean visible) {
    // Some implementation here
}
  • 按照惯例,首先我们需要注意的是,Java中的方法的命名要遵循驼峰规则,比如:setTitleVisible。选择这样命名是非常好的一种习惯并且这样可以通过方法名就能够描述方法的职能以及该方法可以做什么。

  • 第二,每个参数的名称都需要表明(至少能够暗示)它的意图。为方法参数找一个正确的、可已说明参数用意的名称是非常重要的,而并不是简单的声明int i, String s, boolean f。

  • 第三,我们上面所声明的方法只有3个参数。虽然Java对于方法参数的数量有最高的限制,但是推荐的方法参数数量低于6个。超过这个点的方法签名将会变得很难理解。

从Java 5版本开始,方法的参数如果是相同类型的变量列表(成为可变参数),则可以使用一种特殊的语法,比如:

public void find(String ... elements) {
    // Some implementation here
}

在内部,Java编译器会转换可变参数为对应类型的一个数组并且在方法实现中可访问这些可变参数。

有趣的是,Java允许使用泛型类型参数声明可变参数。然而,由于参数类型不被知晓,所以Java编译器要确定泛型可变参数被正确的使用并建议将方法声明为final方法,同时使用@SafeVarargs注解注解签名为泛型可变参数的方法。比如:

扫描二维码关注公众号,回复: 885344 查看本文章
@SafeVarargs
final public<T> void find(T ... elements) {
    // Some implementation here
}

另外一种方式就是使用@SuppressWarnings注解,比如:

@SuppressWarnings( "unchecked" )
public<T> void findSuppressed(T ... elements) {
    // Some implementation here
}

我们接下来要演示的就是方法签名部分异常检查的用法。最近几年异常检查已经被证明并没有预期的那样有用,因为异常检查会导致在实际问题尚未解决的情况下写入很多的样板代码。

public void write( File file ) throws IOException {
    // Some implementation here
}

通常建议将方法参数标记为final(但很少使用)。当方法参数重新分配不同的值时,它有助于摆脱不良的代码实践。而且,这种方法参数可以被匿名类使用,尽管Java 8通过引入有效的final变量来缓解这个约束。

三、方法体(Method body)

每个方法都有他自己的实现和存在的意图。但是,有一些通用的指南真的可以帮助你写出简介且可读性很高的方法。

  • 可能最重要的一个就是单一责任原则(single responsibility principle):尽最大的努力使用这种原则去实现方法,这样的话每个方法只做一件事情并且可以做很好。遵循这种原则可能会增添加类的方法数量,所以最重要的是如何去找到正确的权衡方案。

  • 编码和设计的另一个重要的事情是保持方法实现的简短(通常是遵循单一职责原则即可)。简短的方法通常更容易推理,再加上简短的方法可以在一个屏幕上显示,这样的话就能够让阅读代码的人能够更快的理解代码。

  • 最后的建议与使用返回语句有关。如果一个方法返回一些值,尽量减少返回语句被调用的地方的数量(比较深层次的人我们推荐在所有的情况下都使用单一的返回语句)。如果方法拥有过多的返回语句,那么遵循其逻辑流程并修改(或重构)实现就会变得越来越困难。

四、方法重载

方法重载技术通常用于为方法参数(签名)为不同的参数类型或组合的方法提供专用的方法版本。虽然方法名称相同,在方法调用的地方编译器会根据实际的参数值找出正确的可选方法(Java中重载的最好例子就是构造器,方法名相同,但是参数不同),如果编译器未找到正确的方法,则编译器会报编译错误。比如:

public String numberToString(Long number) {
    return Long.toString( number );
}

public String numberToString(BigDecimal number) {
    return number.toString();
}

方法重载有点类似泛型,但是它是用在泛型不适合使用的情况并且每个(或大多数)泛型类型参数都需要自己的专用实现。不过,组合泛型和重载可能会相当强大,但是由于类型擦除的原因,在Java中应该是不可能了。我们来看个例子:

public<T extends Number> String numberToString(T number) {
    return number.toString();
}

public String numberToString(BigDecimal number) {
    return number.toPlainString();
}

虽然上面的代码可以不使用泛型来写,但是为了我们的演示意图这并不是很重要。有趣的部分是numberToString方法重载了一个BigDecimal的专用实现,并为所有其他数字类型提供了一个泛型版本。

五、方法覆盖(Method overriding)

我们在本系列文章的如何设计类和接口一文中我们已经谈论过方法覆盖。在本节,在我们已经了解关于方法覆盖的情况下,我们将会更深入的去探讨为什么使用@Override 注解是如此的重要。我们将会通过例子的形式去演示在简单的类层次结构中方法覆盖和方法重载的细微的差别。

public class Parent {
    public Object toObject(Number number) {
        return number.toString();
    }
}

类Parent有一个方法toObject。我们声明一个子类继承这个类并尝试覆盖toObject方法转化Number为String。

public class Child extends Parent {
    @Override
    public String toObject(Number number) {
        return number.toString();
    }
}

不过,Child类中的toObject方法的签名与父类toObject方法的签名相同,但是返回值类型有一点不同(请参阅Covariant方法的返回类型以获取更多详细信息),但它确实覆盖了父类的方法并且Java编译器对此可以编译通过。现在,让我们为Child类添加一个方法:

public class Child extends Parent {
    public String toObject(Double number) {
        return number.toString();
    }
}

同样,在方法签名(Double而不是Number)方面只有细微的差别,但在这种情况下,它是方法的重载版本,它不覆盖父方法。如果Java编译器和@Override注解的帮助不存在,那么我们上一个例子中使用@Override注解的方法就会报编译错误。

六、内联(Inlining)

内联是由Java JIT(Just-in-time)编译器执行的一种优化,目的是消除特定的方法调用,并直接用方法实现替换它。JIT编译器所使用的思想就是依赖方法被调用的频率和方法的大小,方法太大就不能被有效的内联。内联可以为您的代码提供显著的性能改进并且可以让你的方法更简短。

七、递归(Recursion)

Java中递归是在执行计算时方法自我调用的一种技术。比如,我们来看一个数字求和的例子:

public int sum( int[] numbers ) {
    if( numbers.length == 0 ) {
        return 0;
    } 
    if( numbers.length == 1 ) {
        return numbers[0];
    } else {
        return numbers[0] + sum(Arrays.copyOfRange(numbers,1, numbers.length));
    }
}

上面的这段求和代码是一种非常低效的实现,但是用来演示递归已经足够了。对于递归方法有一个我们所熟知的问题,即依赖于调用链的深度可能会填满堆栈并最终导致StackOverflowError异常。但是事情并非我们所听到的那么糟糕,因为有一种被称为尾部调用优化(tail call optimization)的技术可以消除栈溢出。如果方法是尾递归(tail-recursive,尾递归方法是所有递归调用都是尾调用的方法)方法时这种技术可以被应用。比如,我们重写上面的算法为尾递归算法:

public int sum( int initial, int[] numbers ) {
    if(numbers.length == 0) {
        return initial;
    } 
    if(numbers.length == 1) {
        return initial + numbers[0];
    } else {
        return sum(initial + numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length));
    }
}

不幸的是,目前Java编译器(以及JVM JIT编译器)不支持尾部调用优化,但在Java中编写递归算法时,它仍然是一个需要了解和考虑的非常有用的技术。

八、方法引用(Method References)

通过将Functional概念引入到Java语言中,Java 8向前迈了一大步。Functional概念的基础是将方法作为数据对待,在之前这种概念在Java中是不被支持的(但是,从Java 7开始,JVM和Java标准库就已经有一些特性让这种概念变得可能)。用方法引用,这一切现在都变成了可能。

引用类型 例子
引用静态方法 SomeClass::staticMethodName
引用指定对象的实例方法 someInstance::instanceMethodName
引用某个类型的任意对象的实例方法 SomeType::methodName
引用构造方法 SomeClass::new

让我们通过一个例子的形式来概述方法如何作为参数来传递给另外的方法。

public class MethodReference {
    public static void println( String s ) {
        System.out.println( s );
    }


    public static void main( String[] args ) {
        final Collection< String > strings = Arrays.asList( "s1", "s2", "s3" );
        strings.stream().forEach( MethodReference::println );
    }
}

main方法的最后一行引用println方法在控制台打印字符串集合中的每一个元素并且方法被作为一个参数传递给其他(forEach)方法。

九、不可变性(Immutability)

不变性现在正受到很多关注,Java也不例外。众所周知Java中实现不可变性是很困难的,但这并不意味着它应该被忽略。

在Java中,不可变性就是改变内部状态。作为一个例子,让我们看看JavaBeans规范(http://docs.oracle.com/javase/tutorial/javabeans/),规范非常清楚的表明, setter可以修改包含对象的状态,并且这也是Java开发者们所期望的。

然而,另一种方法是不要修改状态,而是每次都要返回一个新状态。听起来并没有那么恐怖并且新的Java 8的Date/Time API是不可变性的一个很好的例子。让我们看一下下面的代码:

final LocalDateTime now = LocalDateTime.now();
final LocalDateTime tomorrow = now.plusHours(24);

final LocalDateTime midnight = now.withHour(0).withMinute(0).withSecond(0).withNano(0);

对需要修改其状态的LocalDateTime实例的每次调用都将返回新的LocalDateTime实例,并保持原来的一个不变。与旧的Calendar和Date相比,这是API设计惯例中的一次重大转变。

十、方法注释(Method Documentation)

在Java中,特别是如果你开发一些库或者框架,所有的公共方法都应该使用Javadoc工具记录(http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html)。严格的讲,Java不会强求你做这些事情,但是好的注释可以帮助其他开发者理解指定的方法是干什么用的,有哪些参数,哪些假设或者约定被实现,那种类型的异常在什么时候抛出以及有什么返回值。

让我们看看下面的一个例子:

/**
* The method parses the string argument as a signed decimal integer.
* The characters in the string must all be decimal digits, except
* that the first character may be a minus sign {@code ’-’} or plus
* sign {@code ’+’}.
*
* <p>An exception of type {@code NumberFormatException} is thrown if
* string is {@code null} or has length of zero.
*
* <p>Examples:
* <blockquote><pre>
* parse( "0" ) returns 0
* parse( "+42") returns 42
* parse( "-2" ) returns -2
* parse( "string" ) throws a NumberFormatException
* </pre></blockquote>
*
* @param str a {@code String} containing the {@code int} representation to be parsed
* @return the integer value represented by the string
* @exception NumberFormatException if the string does not contain a valid integer value
*/
public int parse( String str ) throws NumberFormatException {
    return Integer.parseInt( str );
}

对于一个简单的parse方法来讲,这个注释相对冗长,但是它展示了Javadoc工具所提供的一些有用的能力,包括其它类的引用,简单的代码段以及一些高级的格式。

使用Javadoc工具生成方法注释,初级到中级的Java开发人员都可以理解方法的意图以及合适的使用方法。

十一、方法参数和返回值

方法注释是很好的一个东西,但是不幸的是,当使用不正确的或不期望的参数值调用方法时,它不会阻止使用。因此,从经验上来讲,所有的公共方法都应该验证它的参数,同时永远不要相信所给的值都是正确的。

回到我们上一节的例子,方法parse在做逻辑操作之前都应该执行方法的参数验证:

public int parse( String str ) throws NumberFormatException {
    if( str == null ) {
        throw new IllegalArgumentException("String should not be null");
    }

    return Integer.parseInt( str );
}

Java有另外一种使用断言(assert)语句的方式执行验证和完整性检查。但是这种方式在运行时可能会被关闭并且不会被执行。这种方式首选用来执行一些检查并抛出相关异常。

即使有方法注释和参数验证,但是有一些和返回值相关的东西任然需要提一下。在Java 8之前,一个方法如果没有值可返回的最简单的方式就是返回null。这也是为什么NullPointerException异常在Java中臭名昭著的原因,Java 8尝试通过引入Optional <T>类来解决这个问题。我们来看个例子:

public<T> Optional<T> find(String id) {
    // Some implementation here
}

Optional <T>提供了很多有用的方法并且完全消除了方法返回null值和无处不在的null值检查的必要。唯一的例外可能就是集合,当方法返回集合时,它总是返回空的集合而不是null,比如:

public<T> Collection<T> find(String id) {
    return Collections.emptyList();
}

十二、方法作为API的关键点

即使您只是在您的机构或者企业中开发应用程序的开发人员,或者是流行的Java框架或库之一的贡献者,但是你所做的设计决策在你的代码如何使用上面都扮演了很重要的角色。

虽然API设计指南可以写成很多本书,但是本系列文章的这一部分我们会接触一些(方法作为API的关键点),因此快速的总结是非常有用的。

  • 方法及方法参数要使用有意义的名称;

  • 尽量保持方法参数少于6个;

  • 保持方法简短且有很强的可读性;

  • 注释公共方法,包括先决条件和样例;

  • 执行参数验证和完整性检查;

  • 避免使用null作为返回值;

  • 只要有意义,尽量设计不可变的方法;

  • 使用可见性和可访问性规则隐藏不公开的方法;

本文我们概述了方法设计的一些关键点,谈论如何有效地使用Java语言编写可读,干净、有效的方法。在下文中,我们将讨论一些编程指南,这些指南旨在帮助您成为更好的Java开发人员。

猜你喜欢

转载自blog.csdn.net/zyhlwzy/article/details/79084345