本文是我们名为“ 高级Java ”的学院课程的一部分。
本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看 !
目录
- 1.简介 2.可变范围 3.类字段和局部变量 4.方法参数和局部变量 5.装箱和拆箱 6.接口 7.琴弦 8.命名约定 9.标准库 10.不变性 11.测试 12.接下来是什么 13.下载源代码
1.简介
在本部分的教程中,我们将继续讨论Java良好编程风格和健壮设计的一般原理。 我们已经在本教程的前面部分中看到了其中一些原则,但是在此过程中将引入许多新的实用建议,以提高您作为Java开发人员的技能。
2.可变范围
在本教程的第3部分 , 如何设计类和接口中 ,我们讨论了如何将可见性和可访问性应用于类和接口成员,从而限制了它们的范围。 但是,我们尚未讨论在方法实现中使用的局部变量。
在Java语言中,每个局部变量(一旦声明)都有一个作用域。 从声明位置到声明的方法(或代码块)的末尾,该变量将变为可见。因此,只有一条规则可遵循:将局部变量声明为靠近其所在的位置尽可能使用。 让我们看一些典型的例子:
for( final Locale locale: Locale.getAvailableLocales() ) {// Some implementation here
}try( final InputStream in = new FileInputStream( "file.txt" ) ) {// Some implementation here
}
在这两个代码段中,局部变量的范围都限于它们在其中声明的执行块。一旦块结束,局部变量将超出范围,并且不再可见。 看起来简洁明了,但是随着Java 8的发布和lambda的引入,使用局部变量的许多众所周知的习惯用法已经过时了。 让我们重写前面示例中的for-each
循环,改为使用lambda:
Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> {// Some implementation here}
);
局部变量成为函数的参数,该函数本身作为forEach
方法的参数传递。
3.类字段和局部变量
Java中的每个方法都属于某个类(如果是Java 8,则该接口属于某个接口,并且该方法被声明为default
)。 这样,在方法实现中使用的局部变量与类成员之间可能会发生名称冲突。 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
的局部变量,并以此隐藏具有相同名称的类成员。 第08
行本应该将类成员和局部变量相加,但是它所做的却非常不同。 正确的版本可能如下所示(使用关键字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允许用不同的值重新分配非final
方法参数(但是,它对原始值没有任何影响)。 例如:
public String sanitize( String str ) {if( !str.isEmpty() ) {str = str.trim();}str = str.toLowerCase();return str;
}
它不是一段漂亮的代码,但足以说明问题:将方法参数str
重新分配给另一个值(基本上用作局部变量)。 在所有情况下(无一例外),可以并且应该避免这种模式(例如,通过将方法参数声明为final
)。 例如:
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部分“ 如何以及何时使用Generics”中 ,我们已经在讨论原始类型包装器作为泛型类型参数时看到了它的实际作用。
尽管Java编译器试图通过执行自动装箱来尽力隐藏这些转换,但有时它会使情况变得更糟并导致意外的结果。 让我们看一下这个例子:
public static void calculate( final long value ) {// Some implementation here
}
final Long value = null;calculate( value );
上面的代码段可以很好地编译,但是当Long
和long
之间的转换发生时,它将在第02
行抛出NullPointerException
。 这里的建议是更喜欢使用原始类型(但是,我们已经知道,这并不总是可能的)。
6.接口
在本教程的第3部分“ 如何设计类和接口”中 ,我们讨论了基于接口和基于契约的开发,并着重强调了在可能的情况下,接口应优先于具体类的事实。 本节的目的是通过展示真实的示例来说服您再有时间首先考虑接口。
接口不依赖于任何特定的实现(默认方法是一个例外)。 它们只是合同,因此,它们在履行合同的方式上提供了很多自由和灵活性。 当实施涉及外部系统或服务时,这种灵活性变得越来越重要。 让我们看一下以下简单接口及其可能的实现:
public interface TimezoneService {TimeZone getTimeZone( final double lat, final double lon ) throws IOException;
}
public class TimezoneServiceImpl implements TimezoneService {@Overridepublic 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/ )来检索特定位置的时区。 但是,由于联系方式是由界面驱动的,因此很容易引入另一个使用数据库或什至平面文件的实现。 这样,接口可以极大地帮助设计可测试的代码。 例如,在每次测试运行中调用外部服务并不总是可行的,因此提供替代的虚拟实现(也称为存根或模拟)是有意义的:
public class TimezoneServiceTestImpl implements TimezoneService {@Overridepublic TimeZone getTimeZone(final double lat, final double lon) throws IOException {return TimeZone.getDefault();}
}
此实现可在需要TimezoneService
接口的每个地方使用,从而将测试方案与对外部组件的依赖隔离开来。
在Java标准集合库中封装了许多适当使用接口的出色示例。 Collection
, List
, Set
,所有这些接口都有几种实现的支持,当倾向于使用合同时,这些实现可以无缝且可互换地替换,例如:
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 >( /* ... */ ) );
print( new Vector< Long >( /* ... */ ) );
7.琴弦
字符串是Java以及大多数编程语言中使用最广泛的类型之一。 Java语言通过本机支持串联和比较,大大简化了字符串常规操作。 另外,Java标准库提供了许多不同的类来提高字符串操作的效率,这就是我们将在本节中讨论的内容。
在Java中,字符串是不可变的对象,以UTF-16格式表示。 每次连接字符串(或执行任何修改原始字符串的操作)时,都会创建String
类的新实例。 因此,连接操作可能会变得非常无效,从而导致创建许多中间字符串实例(通常来说,会生成垃圾)。
但是Java标准库提供了两个非常有用的类,旨在促进字符串操作: StringBuilder
和StringBuffer
(它们之间的唯一区别是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( "%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%
String.format
方法提供了一种干净方便的方法来从不同的数据类型构造字符串。 值得一提的是,某些现代Java IDE能够根据传递给String.format
方法的参数来分析格式规范,并在检测到任何不匹配时向开发人员发出警告。
8.命名约定
Java作为一种语言并没有强迫开发人员严格遵守任何命名约定,但是社区已经开发了一套易于遵循的规则,这些规则使Java代码在标准库和所有其他Java项目中看起来都是一致的。
- 软件包名称以小写形式输入:
org.junit
,com.fasterxml.jackson
,javax.json
- 类,枚举,接口或注释名称以大写字母键入:
StringBuilder
,Runnable
,@Override
- 方法或字段名 (
static final
除外)以驼峰形式输入:isEmpty
,format
,addAll
- 静态最终字段或枚举常量名称以大写字母键入, 并用下划线 “ _”
MIN_RADIX
:LOG
,MIN_RADIX
,INSTANCE
- 局部变量和方法参数名称以驼峰式键入:
str
,newLength
,minimumCapacity
- 泛型类型参数名称通常以大写形式表示为一个字符 :
T
,U
,E
通过遵循这些简单的约定,您正在编写的代码将与其他任何库或框架看起来简洁明了,并没有区别,给人的印象是它是由同一人创作的(约定真正起作用的罕见情况之一)。
9.标准库
无论您从事哪种Java项目,Java标准库都是您最好的朋友。 是的,很难不同意它们有一些粗糙的边缘和奇怪的设计决策,但是在99%的情况下,这是专家编写的高质量代码。 值得学习。
每个Java版本都为现有库带来了许多新功能(可能会淘汰旧功能),并增加了许多新库。 Java 5带来了在java.util.concurrent
包下协调的新并发库。 Java 6提供了(很少javax.script
知道)脚本支持( javax.script
包)和Java编译器API(在javax.tools
包下)。 Java 7对java.util.concurrent
了很多改进,在java.nio.file
包下引入了新的I / O库,并通过java.lang.invoke
包支持了动态语言。 最后,Java 8提供了一个期待已久的日期/时间API,该API在java.time
程序包下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 {@Testpublic void testNumberFormattingWithLeadingZeros() {final String formatted = String.format( "%04d", 1 );assertThat( formatted, equalTo( "0001" ) );}@Testpublic void testDoubleFormattingWithTwoDecimalPoints() {final String formatted = String.format( "%.2f", 12.324234d );assertThat( formatted, equalTo( "12.32" ) );}
}
测试看起来非常易读,其执行是实例化。 如今,普通的Java项目包含数百个测试用例,为开发人员提供了有关正在开发的回归或功能的快速反馈。
12.接下来是什么
本教程的这一部分完成了与Java编程实践和指南相关的一系列讨论。 在下一部分中,我们将通过探索Java异常的世界,其类型,使用方式和使用时间来返回到语言功能。
13.下载源代码
这是关于“通用编程准则”的课程,是“高级Java”课程的课程。 您可以在此处下载源代码: AdvancedJavaPart7
翻译自: https://www.javacodegeeks.com/2015/09/general-programming-guidelines.html