杰瑞科技汇

高效Java,如何写出更高效的代码?

Of course! "Efficient Java" is a broad topic that encompasses several key areas. Here’s a comprehensive guide broken down into core principles, specific language features, and modern practices.

高效Java,如何写出更高效的代码?-图1
(图片来源网络,侵删)

Part 1: The Foundational Principles of Efficiency

These are the timeless rules that apply regardless of the Java version. Ignoring these is the most common source of performance problems.

Choose the Right Data Structure

This is arguably the most important factor. Using the wrong collection can lead to O(n²) algorithms where O(log n) or O(1) was possible.

Scenario Good Choice Why? Bad Choice Why?
Need fast insertion/deletion LinkedList O(1) for add/remove at ends. ArrayList O(n) for insertion/deletion in the middle.
Need fast random access ArrayList O(1) for get(index). LinkedList O(n) for get(index).
Need unique elements, fast lookup HashSet O(1) average for add, contains, remove. ArrayList O(n) for contains and remove.
Need key-value pairs, fast lookup HashMap O(1) average for get, put, remove. ArrayList of pairs O(n) for lookup.
Need to preserve insertion order LinkedHashSet, LinkedHashMap O(1) with order tracking. HashSet Order is not guaranteed.
Need sorted order TreeSet, TreeMap O(log n) for add, contains, remove. HashSet Unsorted.

Pro Tip: If you only need to iterate over a collection and don't need random access, prefer an ArrayList over a LinkedList. The memory locality of an ArrayList (a single contiguous block of memory) makes it much faster for iteration, even though LinkedList has better theoretical performance for some operations.

Beware of Object Creation (Garbage Collection)

Object creation is not free. It consumes memory and triggers Garbage Collection (GC), which can pause your application.

高效Java,如何写出更高效的代码?-图2
(图片来源网络,侵删)
  • Prefer Primitive Types: Use int, double, boolean instead of their wrapper classes (Integer, Double, Boolean) when you can. Autoboxing/unboxing can create many unnecessary objects.

  • Reuse Objects: Instead of creating new objects in a loop, create them once and reuse them. This is especially important for small, frequently used objects.

    // Bad: Creates many String objects in the loop
    for (int i = 0; i < 1000; i++) {
        String s = new String("Processing item " + i);
        // ...
    }
    // Good: Reuses the StringBuilder
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++) {
        sb.setLength(0); // Clear the buffer
        sb.append("Processing item ").append(i);
        String s = sb.toString();
        // ...
    }
  • Use StringBuilder for String Concatenation: In loops, using for strings creates intermediate StringBuilder objects and many intermediate String objects. StringBuilder is much more efficient.

    // Bad
    String result = "";
    for (int i = 0; i < 1000; i++) {
        result += "some text"; // Creates a new String object each time
    }
    // Good
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 1000; i++) {
        sb.append("some text");
    }
    String result = sb.toString();

Minimize I/O Operations

I/O (disk, network) is thousands of times slower than CPU operations.

高效Java,如何写出更高效的代码?-图3
(图片来源网络,侵删)
  • Buffering: Always wrap streams in buffered versions (BufferedReader, BufferedWriter, BufferedInputStream).

    // Bad
    FileReader fr = new FileReader("large_file.txt");
    // Reads one character at a time - very slow
    // Good
    BufferedReader br = new BufferedReader(new FileReader("large_file.txt"));
    // Reads a large chunk of data into memory at once
  • Batching: Instead of writing many small pieces of data, write them in larger batches.

Use StringBuilder for Logging in Loops

Avoid calling logger.debug() or logger.info() inside a tight loop if the log level is set higher (e.g., INFO or WARNING). The string concatenation and method call overhead can be significant.

// Bad: String concatenation happens even if the log is not printed
for (Item item : items) {
    logger.debug("Processing item with ID: " + item.getId());
}
// Good: Use a guard clause or a lambda with parameterized logging
for (Item item : items) {
    if (logger.isDebugEnabled()) {
        logger.debug("Processing item with ID: {}", item.getId());
    }
}
// Even better with modern SLF4J:
for (Item item : items) {
    logger.debug("Processing item with ID: {}", item.getId()); // No string concat if not debug
}

Part 2: Language & API-Specific Efficiency

String, StringBuilder, and StringBuffer

  • String is Immutable: This is a core feature. It's safe for sharing across threads and can be used as HashMap keys. However, every modification creates a new object.
  • StringBuilder is Mutable: Not thread-safe, but faster than StringBuffer. Use it for all non-thread-local string building.
  • StringBuffer is Mutable & Thread-Safe: Synchronized methods make it slower. Only use it if you absolutely need thread safety for string building (which is rare).

equals(), hashCode(), and compareTo()

  • Override equals() and hashCode() together: If you override equals() and two objects are equal, their hashCode() must be the same. Failure to do so will break collections like HashMap and HashSet.
  • Implement Comparable for Sorting: If your objects will be stored in a TreeSet or sorted with Collections.sort(), implement the Comparable interface. The compareTo() method is generally faster than using a Comparator.

Leverage the Java Stream API (Java 8+)

The Stream API can be very efficient, but you must use it correctly.

  • Use filter and map for Lazy Evaluation: Operations like filter() and map() are intermediate operations and are lazy. They don't execute until a terminal operation (like collect(), forEach(), count()) is called. This allows for short-circuiting and optimization.
    list.stream()           // 1. Create a stream
         .filter(s -> s.length() > 5) // 2. Lazily filter. Not executed yet.
         .map(String::toUpperCase)  // 3. Lazily map. Not executed yet.
         .collect(Collectors.toList()); // 4. Terminal operation. NOW the pipeline is executed.
  • Beware of forEach vs. for-each loop: For simple iterations, a traditional for-each loop is often faster because it has less overhead. Use forEach when you need to leverage a functional style or parallel streams.
  • Parallel Streams (parallelStream()): Can provide a performance boost for CPU-intensive operations on large collections. However, they are not a magic bullet. For simple operations or small datasets, the overhead of splitting and managing threads can make them slower. Avoid using them with I/O-bound operations.

Part 3: Modern Java (Java 8+) and Concurrency

Optional<T> (Java 8+)

Optional is a container object that may or may not contain a non-null value. It helps eliminate NullPointerException and makes your API clearer.

  • Don't use Optional.get() directly. This defeats the purpose and can throw a NoSuchElementException.

  • Use ifPresent(), orElse(), orElseGet():

    // Good: Provides a default value if the optional is empty
    String value = optionalValue.orElse("default_value");
    // Good: Lazily creates a default value if needed
    String value = optionalValue.orElseGet(() -> expensiveComputation());
    // Good: Performs an action only if a value is present
    optionalValue.ifPresent(val -> System.out.println("Found: " + val));

Concurrency: synchronized vs. java.util.concurrent

  • synchronized Block: The simplest way to ensure thread safety. However, it can be a performance bottleneck as it locks the entire object/monitor.
  • java.util.concurrent Package: This package is full of highly efficient, concurrent data structures designed for multi-threaded environments.
    • ConcurrentHashMap: A high-performance thread-safe HashMap. It uses fine-grained locking (lock striping) to allow multiple threads to read and write concurrently.
    • CopyOnWriteArrayList: Excellent for scenarios where you have many readers and very few writers. When a write occurs, it creates a new copy of the underlying array. Iteration is very fast and never throws ConcurrentModificationException.
    • Atomic Classes (AtomicInteger, AtomicReference): Use these for simple, atomic operations on single variables. They are lock-free and highly performant.

Part 4: Profiling and Measurement

You can't optimize what you don't measure. Premature optimization is the root of all evil.

  1. Use a Profiler: Tools like VisualVM (bundled with the JDK), JProfiler, or YourKit are essential. They can show you:

    • CPU Hotspots: Which methods are consuming the most CPU time.
    • Memory Allocation: Where objects are being created and if there are memory leaks.
    • Thread Contention: Where threads are waiting for locks.
  2. Benchmark with JMH (Java Microbenchmark Harness): For micro-optimizations (e.g., HashMap vs. LinkedHashMap performance), you need a proper tool. JMH runs your code in a loop, warms up the JVM to avoid JIT compilation skew, and gives you reliable, accurate results.

Summary: A Quick Checklist

Area Do This Don't Do This
Data Structures Use ArrayList for random access, HashMap for key-value lookups. Use ArrayList for frequent contains/remove operations.
Memory Reuse objects. Use primitives. Use StringBuilder. Create objects in loops. Use for string concatenation in loops.
I/O Always use buffered streams (BufferedReader). Read/write one byte/character at a time without buffering.
Strings Use StringBuilder for building strings. Use for concatenation in performance-critical code.
Concurrency Use ConcurrentHashMap for shared maps. Use synchronized on a whole method when a small block is enough.
APIs Use Optional to represent optional values. Use Optional.get() or pass null explicitly.
General Profile first, then optimize. Guess where the bottleneck is and start changing code.
分享:
扫描分享到社交APP
上一篇
下一篇