如何有效地编写方法

本文是我们名为“ 高级Java ”的学院课程的一部分。

本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看

1.简介

在本教程的这一部分中,我们将花费一些时间来讨论与Java设计和实现方法有关的不同方面。 正如我们在本教程的前面部分所看到的那样,用Java编写方法非常容易,但是有很多事情可以使您的方法更具可读性和效率。

2.方法签名

众所周知,Java是一种面向对象的语言。 这样,Java中的每个方法都属于某个类实例(如果使用static方法,则属于一个类本身),具有可见性(或可访问性)规则,可以声明为abstractfinal等。 但是,可以说方法的最重要部分是它的签名:返回类型和参数,以及方法实现可能抛出的已检查异常的列表(但是如今,这一部分的使用越来越少了)。 下面是一个小例子:

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

main方法将字符串数组作为唯一参数args接受,并且不返回任何内容。 使所有方法保持与main一样简单将是非常好的。 但实际上,方法签名可能变得不可读。 让我们看下面的例子:

public void setTitleVisible( int lenght, String title, boolean visible ) {
    // Some implementation here
}

首先要注意的是,按照惯例,Java中的方法名称以驼峰形式编写,例如: setTitleVisible 。 该名称选择得当,并试图描述该方法应该执行的操作。

其次,每个参数的名称说明(或至少暗示)其目的。 为方法参数找到正确的解释性名称非常重要,而不是int iString sboolean f (但是在极少数情况下还是有意义的)。

第三,该方法仅接受三个参数。 尽管Java对允许的参数数量有更高的限制,但强烈建议将此数字保持在6以下。除此之外,方法签名变得很难理解。

从Java 5发行版开始,这些方法可以使用特殊语法具有相同类型的参数变量列表(称为varargs),例如:

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

在内部,Java编译器将varargs转换为相应类型的数组,这就是方法实现可以访问varargs的方式。

有趣的是,Java还允许使用泛型类型参数声明varargs参数。 但是,由于参数的类型未知,因此Java编译器希望确保负责任地使用varargs ,并建议该方法是final并使用@SafeVarargs批注进行批注(有关批注的更多详细信息,请参见第5部分)。教程, 以及如何以及何时使用Enums和Annotations )。 例如:

@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 。 当用不同的值重新分配方法参数时,它有助于摆脱不良的代码实践。 同样,匿名类可以使用这种方法参数(有关匿名类的更多详细信息,在本教程的第3部分“ 如何设计类和接口”中进行了介绍 ),尽管Java 8通过有效地引入final变量来缓解了这一限制。

3.方法主体

每种方法都有其自己的实现和目的。 但是,有一些通用指南确实可以帮助编写清晰易懂的方法。

可能最重要的一个是单一责任原则:尝试以这种方式实现方法,即每个单一方法都只会做一件事情并且做得很好。 遵循此原理可能会炸毁许多类方法,因此找到合适的平衡很重要。

编码和设计时的另一重要事项是保持方法实现的简短(通常只需遵循单一职责原则,您就可以免费获得它)。 简短的方法很容易推论,而且它们通常适合一个屏幕,因此您的代码读者可以更快地理解它们。

最后(但并非最不重要)的建议与使用return语句有关。 如果某个方法返回某个值,请尝试最大程度地减少调用return语句的位置(有些人走得更远,建议在所有情况下仅使用单个return语句)。 更多的return语句方法变得很难遵循其逻辑流程并修改(或重构)实现。

4.方法重载

方法重载技术通常用于为不同的参数类型或组合提供方法的专用版本。 尽管方法名称保持不变,但是编译器会根据调用点的实际参数值选择正确的替代方法(重载的最佳示例是Java中的构造函数:名称始终相同,但参数集不同)或如果找不到,将引发编译器错误。 例如:

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

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

方法重载在某种程度上接近于泛型(关于泛型的更多详细信息在本教程的第4部分“ 如何以及何时使用泛型”中进行了介绍 ),但是它在泛型方法不能很好地工作并且每种(或大多数)泛型都适用的情况下使用类型参数需要它们自己的专用实现。 尽管如此,将泛型和重载结合起来可能非常强大,但是由于类型擦除(在Java中通常是不可能的)(有关更多详细信息,请参阅教程的第4部分如何及何时使用泛型 )。 让我们看一下这个例子:

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

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

尽管可以在不使用泛型的情况下编写此代码段,但对于我们的演示目的而言,它不是重要的。 有趣的是,方法numberToString重载了BigDecimal的专门实现,并为所有其他数字提供了通用版本。

5.方法覆盖

在本教程的第3部分“ 如何设计类和接口”中 ,我们讨论了很多方法重写。 在本节中,当我们已经知道方法重载时,我们将展示为什么使用@Override批注如此重要。 我们的示例将演示简单类层次结构中方法重写和重载之间的细微差别。

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

Parent类只有一个toObject方法。 让我们对该类进行子类化,并尝试提出方法版本以将数字转换为字符串(而不是原始对象)。

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

尽管如此, Child类中toObject方法的签名还是有些不同(请参见Covariant方法返回类型以获取更多详细信息),它确实覆盖了超类中的那个,并且Java编译器对此没有任何抱怨。 现在,让我们向Child类添加另一个方法。

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

同样,方法签名之间只有细微的差别(用Double代替Number ),但是在这种情况下,它是方法的重载版本,它不会覆盖父方法。 这就是Java编译器和@Override注释的帮助得到回报的时候:用@Override注释上一个示例中的方法会引发编译器错误。

6.内联

内联是Java JIT(即时)编译器执行的优化,目的是消除特定的方法调用并将其直接替换为方法实现。 JIT编译器使用的启发式方法取决于方法的调用频率和大小。 太大的方法不能有效地内联。 内联可以大大提高代码的性能,这是使方法保持简短的另一个好处,如我们在“ 方法正文 ”一节中所讨论的。

7.递归

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异常。 但是事情并没有听起来那么糟糕,因为有一种技术可以消除栈溢出,称为尾部调用优化 。 如果方法是尾部递归的,则可以应用此方法(尾部递归方法是所有递归调用都是尾部调用的方法)。 例如,让我们以尾递归的方式重写以前的算法:

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编写递归算法时,它仍然是了解和考虑的一种非常有用的技术。

8.方法参考

通过将功能性概念引入Java语言,Java 8向前迈出了一大步。 其基础是将方法视为数据,这是该语言以前所不支持的概念(但是,由于Java 7,JVM和Java标准库已经具有使之成为可能的某些功能)。 幸运的是,有了方法引用,现在就可以了。

参考类型
引用静态方法 SomeClass::staticMethodName
引用特定对象的实例方法 someInstance::instanceMethodName
引用特定类型的任意对象的实例方法 SomeType::methodName
引用构造函数 SomeClass::new

表格1

让我们看一个简单的示例,该示例说明如何将方法作为参数传递给其他方法。

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

9.不变性

如今,不变性已引起了很多关注,Java也不例外。 众所周知,不变性在Java中很难实现,但这并不意味着应将其忽略。

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

但是,替代方法不是修改状态,而是每次都返回一个新状态。 它并不像听起来那样可怕,新的Java 8 Date / Time API (在JSR 310:Date and 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实例,并使原始实例保持不变。 与旧的CalendarDate相比,API设计范例发生了很大的变化(使用起来不太舒服,并且引起很多麻烦)。

10.方法文档

在Java中,特别是如果您正在开发某种库或框架,则应使用Javadoc工具记录所有公共方法( http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html ) 。 严格来说,没有什么可以强迫您执行此操作,但是好的文档可以帮助其他开发人员了解特定方法的作用,所需的参数,其实现所具有的假设或约束,异常的类型以及何时可以引发以及返回值(如果有)可能是(加上更多东西)。

让我们看下面的例子:

/**
 * 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工具提供的一些有用功能,包括对其他类的引用,示例代码片段和高级格式化。 这是流行的Java IDE之一Eclipse反映此方法文档的方式。

6,Javadoc.Eclipse

仅通过查看上面的图片,任何初级到高级的Java开发人员都可以了解该方法的目的以及使用该方法的正确方法。

11.方法参数和返回值

文档化方法是一件很了不起的事情,但是不幸的是,当使用不正确或意外的参数值调用方法时,它不能阻止用例。 因此,根据经验,所有公共方法都应验证其自变量,并且永远不应相信将始终使用正确的值来指定它们(这种模式称为健全性检查)。

回到上一节的示例,方法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 。 这就是Java对于NullPointerException异常如此臭名昭著的原因。 Java 8试图通过引入Optional < T >类来解决此问题。 让我们看一下这个例子:

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

Optional < T >提供了许多有用的方法,并且完全消除了该方法返回null并在各处使用null检查污染代码的需要。 唯一的例外可能是集合。 每当方法返回集合时,总是最好返回空的而不是null (甚至是Optional < T > ),例如:

public&lt; T &gt; Collection&lt; T &gt; find( String id ) {
return Collections.emptyList();
}

12.方法作为API入口点

即使您只是在组织内构建应用程序的开发人员,还是对流行的Java框架或库之一的贡献者,您正在执行的设计决策在如何使用代码方面都起着非常重要的作用。

尽管API设计指南值得多本书籍,但本教程的这一部分涉及其中的许多内容(因为方法成为API的切入点),因此快速总结将非常有帮助:

13.接下来是什么

本教程的这一部分讨论的不是Java语言,而是更多关于如何有效地使用Java语言,特别是通过编写可读,干净,有文档的有效方法。 在下一节中,我们将继续相同的基本思想,并讨论通用的编程准则,以帮助您成为更好的Java开发人员。

14.下载源代码

这是关于如何有效地编写方法的课程。 您可以在此处下载源代码: advanced-java-part-6

翻译自: https://www.javacodegeeks.com/2015/09/how-to-write-methods-efficiently.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值