Performance Principles
Premature Optimization
"Premature optimization is the root of all evil." - Donald Knuth
Always measure first. Optimize only after profiling identifies actual bottlenecks.
Optimization Process
- Write correct, clean code first
- Measure performance with realistic data
- Identify bottlenecks using profilers
- Optimize the critical path
- Measure again to verify improvement
String Operations
StringBuilder for Concatenation
// BAD: String concatenation in loop - O(n²)
String result = "";
for (String item : items) {
result += item + ", "; // Creates new String each iteration!
}
// GOOD: StringBuilder - O(n)
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item).append(", ");
}
String result = sb.toString();
// BETTER: String.join() for simple cases
String result = String.join(", ", items);
// BEST: Streams with Collectors.joining()
String result = items.stream()
.collect(Collectors.joining(", "));
// Note: Single concatenation is fine - compiler optimizes it
String msg = "Hello, " + name + "!"; // OK
String Interning
// String literals are automatically interned
String s1 = "hello";
String s2 = "hello";
s1 == s2; // true - same object in string pool
// new String() creates new object
String s3 = new String("hello");
s1 == s3; // false - different objects
// Manual interning (use sparingly)
String s4 = s3.intern();
s1 == s4; // true - s4 points to pool
Collection Performance
Choosing the Right Collection
// ArrayList vs LinkedList
// ArrayList: Fast random access O(1), slow insert/delete O(n)
List<String> arrayList = new ArrayList<>();
// LinkedList: Slow random access O(n), fast insert/delete O(1)
// In practice, ArrayList is almost always faster due to CPU cache
// HashMap vs TreeMap
// HashMap: O(1) average for get/put
Map<String, User> hashMap = new HashMap<>();
// TreeMap: O(log n) for get/put, but maintains sorted order
Map<String, User> treeMap = new TreeMap<>();
// HashSet vs TreeSet - same tradeoffs
Set<String> hashSet = new HashSet<>(); // O(1)
Set<String> treeSet = new TreeSet<>(); // O(log n), sorted
Initial Capacity
// BAD: Default capacity causes resizing
List<String> list = new ArrayList<>(); // Initial capacity 10
for (int i = 0; i < 10000; i++) {
list.add(item); // Multiple resizes!
}
// GOOD: Pre-size when you know the size
List<String> list = new ArrayList<>(10000);
// HashMap: Account for load factor (default 0.75)
int expectedSize = 1000;
Map<String, Object> map = new HashMap<>(
(int) (expectedSize / 0.75) + 1
);
Avoid Boxed Primitives in Collections
// BAD: Boxing overhead
List<Integer> numbers = new ArrayList<>();
int sum = 0;
for (Integer n : numbers) {
sum += n; // Unboxing on each iteration
}
// GOOD: Use primitive arrays when possible
int[] numbers = new int[1000];
int sum = 0;
for (int n : numbers) {
sum += n; // No boxing
}
// ALTERNATIVE: Use specialized collections (Eclipse Collections, etc.)
// IntArrayList, LongHashSet, etc.
Stream Performance
// Streams have overhead - avoid for simple operations
// Simple iteration - loop is faster
for (User user : users) {
user.setActive(true);
}
// Complex transformations - streams shine
Map<String, List<Order>> ordersByCustomer = orders.stream()
.filter(Order::isCompleted)
.collect(Collectors.groupingBy(Order::getCustomerId));
// Parallel streams - only for CPU-intensive operations on large datasets
long sum = numbers.parallelStream()
.filter(n -> expensiveCheck(n))
.mapToLong(Long::valueOf)
.sum();
// DON'T parallelize:
// - Small collections (< 10,000 elements)
// - I/O-bound operations
// - Operations with side effects
// Avoid repeated stream creation
// BAD
users.stream().filter(u -> u.isActive()).count();
users.stream().filter(u -> u.isActive()).map(...); // Second traversal!
// GOOD: Collect intermediate results if needed multiple times
List<User> activeUsers = users.stream()
.filter(User::isActive)
.toList();
long count = activeUsers.size();
List<String> names = activeUsers.stream().map(User::getName).toList();
Object Creation
// Reuse immutable objects
// BAD
Boolean flag = new Boolean(true); // Deprecated, creates new object
// GOOD
Boolean flag = Boolean.TRUE; // Reuses cached instance
Integer num = Integer.valueOf(42); // Caches -128 to 127
// Object pooling for expensive objects
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd"); // Create once
public String format(LocalDate date) {
return date.format(FORMATTER); // Reuse
}
// Lazy initialization for expensive objects
private volatile ExpensiveObject instance;
public ExpensiveObject getInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new ExpensiveObject();
}
}
}
return instance;
}
Database Performance
// Use connection pooling
@Bean
public HikariDataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
// Batch operations
// BAD: Individual inserts
for (User user : users) {
userRepository.save(user); // N database calls
}
// GOOD: Batch insert
userRepository.saveAll(users); // Single batched call
// Fetch only what you need
// BAD: Fetch entire entity
List<User> users = userRepository.findAll();
List<String> emails = users.stream()
.map(User::getEmail)
.toList();
// GOOD: Projection
@Query("SELECT u.email FROM User u")
List<String> findAllEmails();
// Pagination for large datasets
Page<User> page = userRepository.findAll(PageRequest.of(0, 20));
Caching
// Spring Cache abstraction
@Service
public class ProductService {
@Cacheable("products")
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "products", allEntries = true)
public void clearCache() { }
}
// In-memory memoization
private final Map<String, Result> cache = new ConcurrentHashMap<>();
public Result compute(String key) {
return cache.computeIfAbsent(key, this::expensiveComputation);
}
Profiling Tools
Essential Profiling Tools
- JProfiler / YourKit - Commercial profilers
- VisualVM - Free, bundled with JDK
- async-profiler - Low-overhead sampling profiler
- JMH - Microbenchmarking harness
- JFR (Java Flight Recorder) - Production profiling
JMH Benchmark Example
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class StringConcatBenchmark {
private List<String> items;
@Setup
public void setup() {
items = IntStream.range(0, 100)
.mapToObj(String::valueOf)
.toList();
}
@Benchmark
public String stringConcat() {
String result = "";
for (String item : items) {
result += item;
}
return result;
}
@Benchmark
public String stringBuilder() {
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item);
}
return sb.toString();
}
}