JavaRush /Java 博客 /Random-ZH /通用编程风格指南
pandaFromMinsk
第 39 级
Минск

通用编程风格指南

已在 Random-ZH 群组中发布
本文是学术课程“高级Java”的一部分。本课程旨在帮助您学习如何有效地使用Java 功能。该材料涵盖“高级”主题,例如对象创建、竞争、序列化、反射等。该课程将教您如何有效掌握 Java 技术。详细信息请参见此处
内容
1. 简介 2. 变量作用域 3. 类字段和局部变量 4. 方法参数和局部变量 5. 装箱和拆箱 6. 接口 7. 字符串 8. 命名约定 9. 标准库 10. 不变性 11. 测试 12. 下一步。 .. 13.下载源代码
一、简介
在本教程的这一部分中,我们将继续讨论 Java 中良好编程风格和响应式设计的一般原则。我们已经在本指南的前面几章中了解了其中一些原则,但还将提供许多实用技巧,旨在提高 Java 开发人员的技能。
2. 变量范围
在第三部分(“如何设计类和接口”)中,我们讨论了在给定范围约束的情况下如何将可见性和可访问性应用于类和接口的成员。但是,我们还没有讨论方法实现中使用的局部变量。在 Java 语言中,每个局部变量一旦声明,就有一个作用域。从声明该变量的位置到方法(或代码块)执行完成的位置,该变量都变得可见。一般来说,唯一要遵循的规则是将局部变量声明为尽可能靠近将要使用它的位置。让我看一个典型的例子: for( final Locale locale: Locale.getAvailableLocales() ) { // блок codeа } try( final InputStream in = new FileInputStream( "file.txt" ) ) { // блока codeа } 在两个代码片段中,变量的范围都限制在声明这些变量的执行块内。当块完成时,作用域结束并且变量变得不可见。这看起来更清楚,但随着 Java 8 的发布和 lambda 的引入,该语言中许多使用局部变量的众所周知的习惯用法正在变得过时。让我举一个使用 lambda 而不是循环的上一个示例: Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> { // блок codeа } ); 您可以看到局部变量已成为函数的参数,而该函数又作为参数传递给 forEach方法。
3. 类字段和局部变量
Java 中的每个方法都属于一个特定的类(或者,在 Java8 中,属于一个接口,该方法被声明为默认方法)。在作为实现中使用的类或方法的字段的局部变量之间,存在名称冲突的可能性。Java 编译器知道如何从可用变量中选择正确的变量,即使多个开发人员打算使用该变量。现代 Java IDE 在通过编译器警告和变量突出显示来告诉开发人员何时将发生此类冲突方面做得非常出色。但在编写代码时考虑这些事情还是更好。我建议看一个例子: public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += value; return value; } } 这个例子看起来很简单,但它是一个陷阱。 calculateValue方法引入了一个局部变量 ,并对其进行操作,隐藏了同名的类字段。该行 value += value; 应该是类字段和局部变量的值之和,但相反,正在执行其他操作。正确的实现如下所示(使用 this 关键字): public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += this.value; return value; } } 虽然此示例在某些方面很幼稚,但它确实演示了一个重要的点,即在某些情况下可能需要数小时来调试和修复。
4. 方法参数和局部变量
缺乏经验的 Java 开发人员经常陷入的另一个陷阱是使用方法参数作为局部变量。Java允许你给非常量参数重新赋值(不过,这对原始值没有影响): public String sanitize( String str ) { if( !str.isEmpty() ) { str = str.trim(); } str = str.toLowerCase(); return str; } 上面的代码片段并不优雅,但是很好地揭示了问题:参数 str被赋值不同的值(基本上用作局部变量)。在所有情况下(没有任何例外),您可以而且应该不使用此示例(例如,通过将参数声明为常量)。例如: public String sanitize( final String str ) { String sanitized = str; if( !str.isEmpty() ) { sanitized = str.trim(); } sanitized = sanitized.toLowerCase(); return sanitized; } 通过遵循这个简单的规则,即使引入局部变量,也可以更轻松地跟踪给定代码并找到问题的根源。
5. 包装与拆箱
装箱和拆箱是 Java 中用于将基本类型( int、long、double 等)转换为相应类型包装器( Integer、Long、Double等) 的技术的名称。在如何以及何时使用泛型教程的第 4 部分中,当我谈到将基本类型包装为泛型的类型参数时,您已经看到了这一点。尽管 Java 编译器尽力通过执行自动装箱来隐藏此类转换,但有时这会低于预期并产生意外结果。让我们看一个例子: public static void calculate( final long value ) { // блок codeа } final Long value = null; calculate( value ); 上面的代码片段编译得很好。 但是,它将在Longlong 之间转换的行上 抛出 NullPointerException。对于这种情况的建议是,建议使用原始类型(但是,我们已经知道这并不总是可能的)。 // блок
6. 接口
在本教程的第 3 部分“如何设计类和接口”中,我们讨论了接口和契约编程,强调接口应尽可能优先于具体类。本节的目的是通过现实生活中的示例来演示这一点,鼓励您首先考虑接口。接口不依赖于特定的实现(默认方法除外)。它们只是合同,例如,它们在合同的执行方式上提供了很大的自由度和灵活性。当实施涉及外部系统或服务时,这种灵活性变得更加重要。让我们看一个简单接口及其可能实现的示例: public interface TimezoneService { TimeZone getTimeZone( final double lat, final double lon ) throws IOException; } public class TimezoneServiceImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { final URL url = new URL( String.format( "http://api.geonames.org/timezone?lat=%.2f&lng=%.2f&username=demo", lat, lon ) ); final HttpURLConnection connection = ( HttpURLConnection )url.openConnection(); connection.setRequestMethod( "GET" ); connection.setConnectTimeout( 1000 ); connection.setReadTimeout( 1000 ); connection.connect(); int status = connection.getResponseCode(); if (status == 200) { // Do something here } return TimeZone.getDefault(); } } 上面的代码片段显示了典型的接口模式及其实现。此实现使用外部 HTTP 服务 ( http://api.geonames.org/ ) 来检索特定位置的时区。然而,因为 契约取决于接口,很容易引入接口的另一个实现,例如使用数据库甚至常规平面文件。有了它们,接口对于设计可测试的代码非常有帮助。例如,在每个测试中调用外部服务并不总是可行,因此实现替代的、最简单的实现(例如存根)是有意义的: 此实现可以在需要 TimezoneService public class TimezoneServiceTestImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { return TimeZone.getDefault(); } }接口的 任何地方使用,从而隔离测试脚本免受外部组件的依赖。Java 标准库中封装了许多有效使用此类接口的优秀示例。集合、列表、集合——这些接口有多种实现方式,可以无缝换出,并且在合约利用时可以互换。例如: public static< T > void print( final Collection< T > collection ) { for( final T element: collection ) { System.out.println( element ); } } print( new HashSet< Object >( /* ... */ ) ); print( new ArrayList< Integer >( /* ... */ ) ); print( new TreeSet< String >( /* ... */ ) );
7. 弦乐
字符串是 Java 和其他编程语言中最常用的类型之一。Java 语言通过直接支持连接和比较操作,简化了许多常规字符串操作。此外,标准库包含许多使字符串操作高效的类。这正是我们本节要讨论的内容。在 Java 中,字符串是以 UTF-16 编码表示的不可变对象。每次连接字符串(或执行任何修改原始字符串的操作)时,都会创建 String类的一个新实例。因此,串联操作可能变得非常低效,导致创建 String类的许多中间实例(通常会产生垃圾)。但是Java标准库包含两个非常有用的类,其目的是使字符串操作变得方便。它们是 StringBuilderStringBuffer(它们之间的唯一区别是 StringBuffer是线程安全的,而 StringBuilder则相反)。让我们看一下使用其中一个类的几个示例: final StringBuilder sb = new StringBuilder(); for( int i = 1; i <= 10; ++i ) { sb.append( " " ); sb.append( i ); } sb.deleteCharAt( 0 ); sb.insert( 0, "[" ); sb.replace( sb.length() - 3, sb.length(), "]" ); 虽然使用 StringBuilder/StringBuffer是操作字符串的推荐方法,但在连接两个或三个字符串的最简单场景中,它可能看起来有些过分,因此正常的加法运算符 ( (“+”),例如:通常, String userId = "user:" + new Random().nextInt( 100 ); 简化连接的最佳替代方法是使用字符串格式化以及 Java 标准库来帮助提供静态 String.format帮助器方法。它支持一组丰富的格式说明符,包括数字、符号、日期/时间等。(有关完整详细信息,请参阅参考文档) String.format 方法 提供 String.format( "%04d", 1 ); -> 0001 String.format( "%.2f", 12.324234d ); -> 12.32 String.format( "%tR", new Date() ); -> 21:11 String.format( "%tF", new Date() ); -> 2014-11-11 String.format( "%d%%", 12 ); -> 12% 了一种干净且轻量级的方法来从各种数据类型生成字符串。值得注意的是,现代 Java IDE 可以从传递给 String.format方法的参数中解析格式规范,并在检测到任何不匹配时向开发人员发出警告。
8. 命名约定
Java 是一种不强迫开发人员严格遵循任何命名约定的语言,但社区制定了一组简单的规则,使 Java 代码在标准库和任何其他 Java 项目中看起来一致:
  • 包名称为小写:org.junit、com.fasterxml.jackson、javax.json
  • 类、枚举、接口、注解的名称均使用大写字母:StringBuilder、Runnable、@Override
  • 字段或方法的名称(除了static final)以驼峰表示法指定:isEmpty、format、addAll
  • static Final 字段或枚举常量名称为大写,并用下划线(“_”)分隔:LOG、MIN_RADIX、INSTANCE。
  • 局部变量或方法参数以驼峰表示法键入:str、newLength、minimumCapacity
  • 泛型的参数类型名称由单个大写字母表示:T、U、E
通过遵循这些简单的约定,您编写的代码将看起来简洁,并且在风格上与其他库或框架没有区别,并且感觉像是由同一个人编写的(这是约定真正起作用的罕见情况之一)。
9. 标准库
无论您正在从事哪种类型的 Java 项目,Java 标准库都是您最好的朋友。是的,很难否认它们有一些粗糙的边缘和奇怪的设计决策,但是,99% 的情况下,它们都是由专家编写的高质量代码。值得探索。每个 Java 版本都为现有库带来了许多新功能(旧功能可能存在一些问题),并且还添加了许多新库。Java 5 带来了一个新的 并发库作为 java.util.concurrent包的一部分。Java 6 引入了(不太为人所知)脚本支持( javax.script包)和Java 编译器 API (作为 javax.tools包的一部分)。Java 7 对 java.util.concurrent带来了许多改进,在 java.nio.file包中引入了新的 I/O 库,并在 java.lang.invoke中引入了对动态语言的支持。最后,Java 8在 java.time包中添加了期待已久的 日期/时间。Java 作为一个平台正在不断发展,随着上述变化的发展是非常重要的。每当您考虑在项目中包含第三方库或框架时,请确保标准 Java 库中尚未包含所需的功能(当然,有许多领先于现有技术的专门的高性能算法实现)标准库中的算法,但在大多数情况下确实不需要)。
10. 不变性
整个指南和本部分的不变性仍然是一个提醒:请认真对待。如果您设计的类或实现的方法可以提供不变性保证,那么它可以在大多数情况下随处使用,而不必担心同时被修改。这将使您作为开发人员的生活(也希望您的团队成员的生活)变得更加轻松。
11. 测试
测试驱动开发 (TDD) 的实践在 Java 社区中非常流行,提高了代码质量的标准。尽管 TDD 提供了所有好处,但令人遗憾的是,今天的 Java 标准库不包含任何测试框架或支持工具。然而,测试已成为现代 Java 开发的必要组成部分,在本节中,我们将了解使用 JUnit框架的一些基本技术。在 JUnit 中,本质上,每个测试都是一组关于对象的预期状态或行为的语句。编写优秀测试的秘诀是保持测试简单、简短,一次测试一件事。作为练习,让我们编写一组测试来验证 String.format是否是字符串部分中返回所需结果的函数。 package com.javacodegeeks.advanced.generic; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.Test; public class StringFormatTestCase { @Test public void testNumberFormattingWithLeadingZeros() { final String formatted = String.format( "%04d", 1 ); assertThat( formatted, equalTo( "0001" ) ); } @Test public void testDoubleFormattingWithTwoDecimalPoints() { final String formatted = String.format( "%.2f", 12.324234d ); assertThat( formatted, equalTo( "12.32" ) ); } } 这两个测试看起来都非常可读,并且它们的执行都是实例。如今,平均 Java 项目包含数百个测试用例,在开发过程中为开发人员提供有关回归或功能的快速反馈。
12. 下一步
本指南的这一部分完成了与 Java 编程实践和该编程语言手册相关的一系列讨论。下次我们将回到该语言的特性,探索 Java 的世界,了解异常、异常类型、如何以及何时使用它们。
13.下载源码
这是高级 Java 课程中关于一般开发原则的课程。 本课程的源代码可以在此处下载。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION