举个例子
// validating/CountedList.java // Keeps track of how many of itself are created. package validating; import java.util.*; public class CountedList extends ArrayList<String> { private static int counter = 0; private int id = counter++; public CountedList() { System.out.println("CountedList #" + id); } public int getId() { return id; } }
// validating/tests/CountedListTest.java // Simple use of JUnit to test CountedList. package validating; import java.util.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; public class CountedListTest { private CountedList list; @BeforeAll static void beforeAllMsg() { System.out.println(">>> Starting CountedListTest"); } @AfterAll static void afterAllMsg() { System.out.println(">>> Finished CountedListTest"); } @BeforeEach public void initialize() { list = new CountedList(); System.out.println("Set up for " + list.getId()); for(int i = 0; i < 3; i++) list.add(Integer.toString(i)); } @AfterEach public void cleanup() { System.out.println("Cleaning up " + list.getId()); } @Test public void insert() { System.out.println("Running testInsert()"); assertEquals(list.size(), 3); list.add(1, "Insert"); assertEquals(list.size(), 4); assertEquals(list.get(1), "Insert"); } @Test public void replace() { System.out.println("Running testReplace()"); assertEquals(list.size(), 3); list.set(1, "Replace"); assertEquals(list.size(), 3); assertEquals(list.get(1), "Replace"); } // A helper method to simplify the code. As // long as it's not annotated with @Test, it will // not be automatically executed by JUnit. private void compare(List<String> lst, String[] strs) { assertArrayEquals(lst.toArray(new String[0]), strs); } @Test public void order() { System.out.println("Running testOrder()"); compare(list, new String[] { "0", "1", "2" }); } @Test public void remove() { System.out.println("Running testRemove()"); assertEquals(list.size(), 3); list.remove(1); assertEquals(list.size(), 2); compare(list, new String[] { "0", "2" }); } @Test public void addAll() { System.out.println("Running testAddAll()"); list.addAll(Arrays.asList(new String[] { "An", "African", "Swallow"})); assertEquals(list.size(), 6); compare(list, new String[] { "0", "1", "2", "An", "African", "Swallow" }); } } /* Output: >>> Starting CountedListTest CountedList #0 Set up for 0 Running testRemove() Cleaning up 0 CountedList #1 Set up for 1 Running testReplace() Cleaning up 1 CountedList #2 Set up for 2 Running testAddAll() Cleaning up 2 CountedList #3 Set up for 3 Running testInsert() Cleaning up 3 CountedList #4 Set up for 4 Running testOrder() Cleaning up 4 >>> Finished CountedListTest */
测试不可能覆盖代码100%的。这里的@...完全可以根据语义理解
前置条件
Design By Contract
断言
assert boolean-expression;
assert boolean-expression: information-expression;
默认是关闭断言的,可以通过类加载器的方式控制断言。这消除了在运行程序时在命令行上使用 -ea 标志的需要,使用 -ea 标志启用断言可能同样简单。
// validating/LoaderAssertions.java // Using the class loader to enable assertions // {ThrowsException} public class LoaderAssertions { public static void main(String[] args) { ClassLoader.getSystemClassLoader(). setDefaultAssertionStatus(true); new Loaded().go(); } } class Loaded { public void go() { assert false: "Loaded.go()"; } } /* Output: ___[ Error Output ]___ Exception in thread "main" java.lang.AssertionError: Loaded.go() at Loaded.go(LoaderAssertions.java:15) at LoaderAssertions.main(LoaderAssertions.java:9) */
原生jdk的比较麻烦,一般是用Guava的
// validating/GuavaAssertions.java // Assertions that are always enabled. import com.google.common.base.*; import static com.google.common.base.Verify.*; public class GuavaAssertions { public static void main(String[] args) { verify(2 + 2 == 4); try { verify(1 + 2 == 4); } catch(VerifyException e) { System.out.println(e); } try { verify(1 + 2 == 4, "Bad math"); } catch(VerifyException e) { System.out.println(e.getMessage()); } try { verify(1 + 2 == 4, "Bad math: %s", "not 4"); } catch(VerifyException e) { System.out.println(e.getMessage()); } String s = ""; s = verifyNotNull(s); s = null; try { verifyNotNull(s); } catch(VerifyException e) { System.out.println(e.getMessage()); } try { verifyNotNull( s, "Shouldn't be null: %s", "arg s"); } catch(VerifyException e) { System.out.println(e.getMessage()); } } } /* Output: com.google.common.base.VerifyException Bad math Bad math: not 4 expected a non-null reference Shouldn't be null: arg s */
契约式设计
1.应该明确指定行为,就好像它是一个契约一样。 2.通过实现某些运行时检查来保证这种行为,他将这些检查称为前置条件、后置条件和不变项。
检查指令
在化学领域,你也许会用一种纯液体去滴定测量另一种液体,当达到一个特定的点时,液体变蓝了。从两个液体的颜色上并不能明显看出;这是复杂反应的一部分。滴定完成后一个有用的检查指令是能够断定液体变蓝了。
检查指令是对你的代码进行补充,当你可以测试并阐明对象或程序的状态时,应该使用它。
前置条件
前置条件确保客户端(调用此方法的代码)履行其部分契约。这意味着在方法调用开始时几乎总是会检查参数(在你用那个方法做任何操作之前)以此保证它们的调用在方法中是合适的。因为你永远无法知道客户端会传递给你什么,前置条件是确保检查的一个好做法。禁用前置条件检查,但除非这是万不得已的情况下。因为这是最不安全、最不明智的选择,因为尽管你知道并且可以控制自己的代码,但是你无法控制客户端可能会传递给方法的参数。然而,某些情况下对性能要求很高,通过分析得到前置条件造成了这个瓶颈,而且你有某种合理的保证客户端不会违反前置条件(比如自己编写客户端的情况下),那么禁用前置条件检查是可接受的。
后置条件
后置条件测试你在方法中所做的操作的结果。这段代码放在方法调用的末尾,在 return 语句之前(如果有的话)。对于长时间、复杂的方法,在返回计算结果之前需要对计算结果进行验证(也就是说,在某些情况下,由于某种原因,你不能总是相信结果),后置条件很重要,但是任何时候你可以描述方法结果上的约束时,最好将这些约束在代码中表示为后置条件。因为不变性检查是观察对象的状态,后置条件检查仅在方法期间验证计算结果,因此可能会被丢弃,以便进行单元测试。一般是不需要的。
不变性invariant
指的是对象状态,你可以只在代码末尾使用不变性检查来编写代码。如果你确信方法主体没有把对象改成无效状态,则可以禁用方法调用末尾的不变性检查。
invariant 指的是对对象的某些约束条件,而 immutable 指的是对象本身是不可变的。比如我们用经纬度来表示地球表面的一个点,这个类是 Point,它有两个参数:经度和纬度。所谓 invariant,就是说这个类的经度参数必须在 -180 到 180 之间,而纬度必须在 -90 到 90 之间。无论是构造器还是 setter 方法都必须验证这个 invariant 条件。所谓 immutable,指的是为了方便地实现线程安全类,我们将 Point 设计为 immutable 的,即经度和纬度属性都是 final 的,且不能提供 setter 方法。一旦实例化一个 point,它就不能再被修改,而只能通过重新 new 一个新的实例来代替旧的 point。
可见,invariant 和 immutable 是无关的,不管一个类是不是 immutable 的,它都必须受到 invariant 条件的制约(即谓不变性),否则它产生的对象就可能是无效的。
class Main { public static void main(String args[]){ int a = 2147483647; int b = 2147483646; System.out.println(add(a, b)); } private static int add(int a, int b) { return a + b; } }
这里输出的是一个负数,因为溢出了,但是这里就没有保证invariant
Dbc+单元测试
1. 前置条件(用于put()):不允许将空元素添加到队列中。
2. 前置条件(用于put()):将元素放入完整队列是非法的。
3. 前置条件(用于get()):试图从空队列中获取元素是非法的。
4. 后置条件用于get()):不能从数组中生成空元素。
5. 不变性:包含对象的区域不能包含任何空元素。
6. 不变性:不包含对象的区域必须只有空值。
// validating/CircularQueue.java // Demonstration of Design by Contract (DbC) package validating; import java.util.*; public class CircularQueue { private Object[] data; private int in = 0, // Next available storage space out = 0; // Next gettable object // Has it wrapped around the circular queue? private boolean wrapped = false; public CircularQueue(int size) { data = new Object[size]; // Must be true after construction: assert invariant(); } public boolean empty() { return !wrapped && in == out; } public boolean full() { return wrapped && in == out; } public boolean isWrapped() { return wrapped; } public void put(Object item) { precondition(item != null, "put() null item"); precondition(!full(), "put() into full CircularQueue"); assert invariant(); data[in++] = item; if(in >= data.length) { in = 0; wrapped = true; } assert invariant(); } public Object get() { precondition(!empty(), "get() from empty CircularQueue"); assert invariant(); Object returnVal = data[out]; data[out] = null; out++; if(out >= data.length) { out = 0; wrapped = false; } assert postcondition( returnVal != null, "Null item in CircularQueue"); assert invariant(); return returnVal; } // Design-by-contract support methods: private static void precondition(boolean cond, String msg) { if(!cond) throw new CircularQueueException(msg); } private static boolean postcondition(boolean cond, String msg) { if(!cond) throw new CircularQueueException(msg); return true; } private boolean invariant() { // Guarantee that no null values are in the // region of 'data' that holds objects: for(int i = out; i != in; i = (i + 1) % data.length) if(data[i] == null) throw new CircularQueueException("null in CircularQueue"); // Guarantee that only null values are outside the // region of 'data' that holds objects: if(full()) return true; for(int i = in; i != out; i = (i + 1) % data.length) if(data[i] != null) throw new CircularQueueException( "non-null outside of CircularQueue range: " + dump()); return true; } public String dump() { return "in = " + in + ", out = " + out + ", full() = " + full() + ", empty() = " + empty() + ", CircularQueue = " + Arrays.asList(data); } }
// validating/tests/CircularQueueTest.java package validating; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; public class CircularQueueTest { private CircularQueue queue = new CircularQueue(10); private int i = 0; @BeforeEach public void initialize() { while(i < 5) // Pre-load with some data queue.put(Integer.toString(i++)); } // Support methods: private void showFullness() { assertTrue(queue.full()); assertFalse(queue.empty()); System.out.println(queue.dump()); } private void showEmptiness() { assertFalse(queue.full()); assertTrue(queue.empty()); System.out.println(queue.dump()); } @Test public void full() { System.out.println("testFull"); System.out.println(queue.dump()); System.out.println(queue.get()); System.out.println(queue.get()); while(!queue.full()) queue.put(Integer.toString(i++)); String msg = ""; try { queue.put(""); } catch(CircularQueueException e) { msg = e.getMessage(); ∂System.out.println(msg); } assertEquals(msg, "put() into full CircularQueue"); showFullness(); } @Test public void empty() { System.out.println("testEmpty"); while(!queue.empty()) System.out.println(queue.get()); String msg = ""; try { queue.get(); } catch(CircularQueueException e) { msg = e.getMessage(); System.out.println(msg); } assertEquals(msg, "get() from empty CircularQueue"); showEmptiness(); } @Test public void nullPut() { System.out.println("testNullPut"); String msg = ""; try { queue.put(null); } catch(CircularQueueException e) { msg = e.getMessage(); System.out.println(msg); } assertEquals(msg, "put() null item"); } @Test public void circularity() { System.out.println("testCircularity"); while(!queue.full()) queue.put(Integer.toString(i++)); showFullness(); assertTrue(queue.isWrapped()); while(!queue.empty()) System.out.println(queue.get()); showEmptiness(); while(!queue.full()) queue.put(Integer.toString(i++)); showFullness(); while(!queue.empty()) System.out.println(queue.get()); showEmptiness(); } } /* Output: testNullPut put() null item testCircularity in = 0, out = 0, full() = true, empty() = false, CircularQueue = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 0 1 2 3 4 5 6 7 8 9 in = 0, out = 0, full() = false, empty() = true, CircularQueue = [null, null, null, null, null, null, null, null, null, null] in = 0, out = 0, full() = true, empty() = false, CircularQueue = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19] 10 11 12 13 14 15 16 17 18 19 in = 0, out = 0, full() = false, empty() = true, CircularQueue = [null, null, null, null, null, null, null, null, null, null] testFull in = 5, out = 0, full() = false, empty() = false, CircularQueue = [0, 1, 2, 3, 4, null, null, null, null, null] 0 1 put() into full CircularQueue in = 2, out = 2, full() = true, empty() = false, CircularQueue = [10, 11, 2, 3, 4, 5, 6, 7, 8, 9] testEmpty 0 1 2 3 4 get() from empty CircularQueue in = 5, out = 5, full() = false, empty() = true, CircularQueue = [null, null, null, null, null, null, null, null, null, null] */
使用Guava前置条件
// validating/GuavaPreconditions.java // Demonstrating Guava Preconditions import java.util.function.*; import static com.google.common.base.Preconditions.*; public class GuavaPreconditions { static void test(Consumer<String> c, String s) { try { System.out.println(s); c.accept(s); System.out.println("Success"); } catch(Exception e) { String type = e.getClass().getSimpleName(); String msg = e.getMessage(); System.out.println(type + (msg == null ? "" : ": " + msg)); } } public static void main(String[] args) { test(s -> s = checkNotNull(s), "X"); test(s -> s = checkNotNull(s), null); test(s -> s = checkNotNull(s, "s was null"), null); test(s -> s = checkNotNull( s, "s was null, %s %s", "arg2", "arg3"), null); test(s -> checkArgument(s == "Fozzie"), "Fozzie"); test(s -> checkArgument(s == "Fozzie"), "X"); test(s -> checkArgument(s == "Fozzie"), null); test(s -> checkArgument( s == "Fozzie", "Bear Left!"), null); test(s -> checkArgument( s == "Fozzie", "Bear Left! %s Right!", "Frog"), null); test(s -> checkState(s.length() > 6), "Mortimer"); test(s -> checkState(s.length() > 6), "Mort"); test(s -> checkState(s.length() > 6), null); test(s -> checkElementIndex(6, s.length()), "Robert"); test(s -> checkElementIndex(6, s.length()), "Bob"); test(s -> checkElementIndex(6, s.length()), null); test(s -> checkPositionIndex(6, s.length()), "Robert"); test(s -> checkPositionIndex(6, s.length()), "Bob"); test(s -> checkPositionIndex(6, s.length()), null); test(s -> checkPositionIndexes( 0, 6, s.length()), "Hieronymus"); test(s -> checkPositionIndexes( 0, 10, s.length()), "Hieronymus"); test(s -> checkPositionIndexes( 0, 11, s.length()), "Hieronymus"); test(s -> checkPositionIndexes( -1, 6, s.length()), "Hieronymus"); test(s -> checkPositionIndexes( 7, 6, s.length()), "Hieronymus"); test(s -> checkPositionIndexes( 0, 6, s.length()), null); } } /* Output: X Success null NullPointerException null NullPointerException: s was null null NullPointerException: s was null, arg2 arg3 Fozzie Success X IllegalArgumentException null IllegalArgumentException null IllegalArgumentException: Bear Left! null IllegalArgumentException: Bear Left! Frog Right! Mortimer Success Mort IllegalStateException null NullPointerException Robert IndexOutOfBoundsException: index (6) must be less than size (6) Bob IndexOutOfBoundsException: index (6) must be less than size (3) null NullPointerException Robert Success Bob IndexOutOfBoundsException: index (6) must not be greater than size (3) null NullPointerException Hieronymus Success Hieronymus Success Hieronymus IndexOutOfBoundsException: end index (11) must not be greater than size (10) Hieronymus IndexOutOfBoundsException: start index (-1) must not be negative Hieronymus IndexOutOfBoundsException: end index (6) must not be less than start index (7) null NullPointerException */
每个前置条件都有三种不同的重载形式:一个什么都没有,一个带有简单字符串消息,以及带有一个字符串和替换值。为了提高效率,只允许 %s (字符串类型)替换标记。在上面的例子中,演示了checkNotNull() 和 checkArgument() 这两种形式。但是它们对于所有前置条件方法都是相同的。注意 checkNotNull() 的返回参数, 所以你可以在表达式中内联使用它。下面是如何在构造函数中使用它来防止包含 Null 值的对象构造:
/ validating/NonNullConstruction.java import static com.google.common.base.Preconditions.*; public class NonNullConstruction { private Integer n; private String s; NonNullConstruction(Integer n, String s) { this.n = checkNotNull(n); this.s = checkNotNull(s); } public static void main(String[] args) { NonNullConstruction nnc = new NonNullConstruction(3, "Trousers"); } }
TDD
纯粹的测试优先编程的主要问题是它假设你事先了解了你正在解决的问题。 根据我自己的经验,我通常是从实验开始,而只有当我处理问题一段时间后,我对它的理解才会达到能给它编写测试的程度。 当然,偶尔会有一些问题在你开始之前就已经完全定义,但我个人并不常遇到这些问题。 实际上,可能用“面向测试的开发 ( Test-Oriented Development )”这个短语来描述编写测试良好的代码或许更好。
日志
// validating/SLF4JLogging.java import org.slf4j.*; public class SLF4JLogging { private static Logger log = LoggerFactory.getLogger(SLF4JLogging.class); public static void main(String[] args) { log.info("hello logging"); } } /* Output: 2017-05-09T06:07:53.418 [main] INFO SLF4JLogging - hello logging */
日志等级
// validating/SLF4JLevels.java import org.slf4j.*; public class SLF4JLevels { private static Logger log = LoggerFactory.getLogger(SLF4JLevels.class); public static void main(String[] args) { log.trace("Hello"); log.debug("Logging"); log.info("Using"); log.warn("the SLF4J"); log.error("Facade"); } } /* Output: 2017-05-09T06:07:52.846 [main] TRACE SLF4JLevels - Hello 2017-05-09T06:07:52.849 [main] DEBUG SLF4JLevels - Logging 2017-05-09T06:07:52.849 [main] INFO SLF4JLevels - Using 2017-05-09T06:07:52.850 [main] WARN SLF4JLevels - the SLF4J 2017-05-09T06:07:52.851 [main] ERROR SLF4JLevels - Facade */
级别设置一般单独放在配置文件里,这样修改就不用重编译。logback使用xml
<!-- validating/logback.xml --> <?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern> %d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n </pattern> </encoder> </appender> <root level="TRACE"> <appender-ref ref="STDOUT" /> </root> </configuration>
调试
输出,日志,debug 本人只用这三个,其实足够了。
基准测试
我们应该忘掉微小的效率提升,说的就是这些 97% 的时间做的事:过早的优化是万恶之源。
一般来讲都是压测。
优化准则
- 避免为了性能牺牲代码的可读性。
- 不要独立地看待性能。衡量与带来的收益相比所需投入的工作量。
- 程序的大小很重要。性能优化通常只对运行了长时间的大型项目有价值。性能通常不是小项目的关注点。
- 运行起来程序比一心钻研它的性能具有更高的优先级。一旦你已经有了可工作的程序,如有必要的话,你可以使用剖析器提高它的效率。只有当性能是关键因素时,才需要在设计/开发阶段考虑性能。
- 不要猜测瓶颈发生在哪。运行剖析器,让剖析器告诉你。
- 无论何时有可能的话,显式地设置实例为 null 表明你不再用它。这对垃圾收集器来说是个有用的暗示。
- static final 修饰的变量会被 JVM 优化从而提高程序的运行速度。因而程序中的常量应该声明 static final。
风格检测checkstyle
错误分析findbugs
代码review
重构refactor
- 测试(通常,JUnit 测试作为最小的根基),因此你能确保重构不会改变代码的行为。
- 自动构建,因而你能轻松地构建代码,运行所有的测试。通过这种方式做些小修改并确保修改不会破坏任何事物是毫不费力的。本书使用的是 Gradle 构建系统,你可以在 代码示例 的 build.gradle 文件中查看示例。
- 版本控制,以便你能回退到可工作的代码版本,能够一直记录重构的每一步。