
java-null-safety
by JongminChung
Systematic notes and summaries from my computer engineering studies.
SKILL.md
name: java-null-safety description: JSpecify null safety annotations with @NullMarked, @Nullable, and package-level configuration allowed-tools: [Read, Edit, Write, Bash, Grep, Glob]
Java Null Safety Skill
Prerequisites
This skill requires JSpecify annotations:
org.jspecify:jspecify(NullMarked, Nullable, NonNull)
Required Imports
// JSpecify Null Safety Annotations
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
Maven Dependency
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
Core Annotations
@NullMarked- Marks a package or class where all types are non-null by default@Nullable- Marks a type as nullable (exception to @NullMarked default)@NonNull- Explicitly marks a type as non-null (only needed without @NullMarked)
Package-Level Configuration (PREFERRED)
Always prefer @NullMarked in package-info.java for consistent null-safety across the entire package.
Correct package-info.java Structure
The package-info.java file has a unique syntax that differs from regular Java classes:
// package-info.java
/*
* Copyright headers and license...
*/
/**
* Token validation and authentication services.
*
* <p>All types in this package are non-null by default due to {@code @NullMarked}.
* Use {@code @Nullable} to explicitly mark nullable types.
*/
@NullMarked
package de.cuioss.portal.authentication;
import org.jspecify.annotations.NullMarked;
CRITICAL: Unique package-info.java Syntax
The structure is special and MUST follow this exact order:
- File header comment (copyright, license)
- Package JavaDoc comment (describes the package)
- Package annotations (like
@NullMarked) packagedeclarationimportstatements (AFTER the package declaration)
Why This Is Different:
In regular Java classes, imports come BEFORE the class declaration:
import java.util.List; // Import first
public class MyClass { // Then class
}
In package-info.java, imports come AFTER the package declaration:
@NullMarked // Annotation first
package com.example; // Then package
import org.jspecify.annotations.NullMarked; // Import last
This reverse ordering is the Java Language Specification requirement for package-info.java files. Placing imports before the package declaration will cause compilation errors.
Benefits:
- Consistent null-safety across entire package
- Less annotation noise (default is non-null)
- Clear contract for package APIs
- Easier to maintain
API Return Type Guidelines
Pattern 1: Guaranteed Non-Null Return (Default)
Methods return non-null by default with package-level @NullMarked:
/**
* Validates the JWT token and returns the result.
*
* @param token the token to validate, must not be null
* @return validation result, never null
*/
public ValidationResult validate(String token) {
// Implementation must ensure non-null return
return new ValidationResult(token, checkSignature(token));
}
Pattern 2: Optional Result
Use Optional<T> when the method may not have a result to return:
/**
* Finds a user by their unique identifier.
*
* @param userId the user identifier, must not be null
* @return the user if found, or Optional.empty() if not found
*/
public Optional<User> findById(String userId) {
User user = repository.get(userId);
return Optional.ofNullable(user);
}
CRITICAL RULE: Never Use @Nullable for Return Types
NEVER use @Nullable for return types. Either guarantee a non-null return or use Optional.
// ❌ BAD - Never do this
public @Nullable ValidationResult validate(String token) {
// Nullable returns are forbidden
}
// ✅ GOOD - Guaranteed non-null
public ValidationResult validate(String token) {
// Must return non-null
}
// ✅ GOOD - Use Optional for "no result" scenarios
public Optional<ValidationResult> tryValidate(String token) {
// Returns Optional.empty() when no result
}
Null-Safe Implementation Patterns
With @NullMarked (Package Level)
// With @NullMarked at package level, everything is non-null by default
public class TokenValidator {
// Field is non-null by default
private final TokenConfig config;
// Parameter is non-null by default
public TokenValidator(TokenConfig config) {
// No null check needed if caller respects contract
// But defensive programming is still acceptable:
this.config = Objects.requireNonNull(config, "config must not be null");
}
// Parameter and return are non-null by default
public ValidationResult validate(String token) {
Objects.requireNonNull(token, "token must not be null");
// Implementation must return non-null
return new ValidationResult(/*...*/);
}
// Mark nullable parameters explicitly
public String processWithDefault(@Nullable String input) {
return input != null ? input.toUpperCase() : "DEFAULT";
}
// Use Optional instead of @Nullable returns
public Optional<UserInfo> extractUserInfo(String token) {
return parseToken(token)
.map(this::extractUser);
}
}
Without @NullMarked (Explicit Annotations)
If not using package-level @NullMarked, you must explicitly annotate:
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
public class TokenValidator {
@NonNull
private final TokenConfig config;
public TokenValidator(@NonNull TokenConfig config) {
this.config = config;
}
@NonNull
public ValidationResult validate(@NonNull String token) {
return new ValidationResult(/*...*/);
}
@NonNull
public String processWithDefault(@Nullable String input) {
return input != null ? input.toUpperCase() : "DEFAULT";
}
}
Implementation Requirements
1. Package-Level Configuration
- Always prefer
@NullMarkedinpackage-info.java - Document the null-safety contract in package documentation
- Use
@Nullableonly for exceptions to the non-null default
2. Default Non-Null
- With
@NullMarked, all types are non-null by default - Only use
@Nullablefor exceptions - Never annotate with
@NonNullwhen using@NullMarked(redundant)
3. Implementation Responsibility
- The implementation MUST ensure that non-nullable methods never return null
- Add defensive null checks at API boundaries
- Use
Objects.requireNonNull()for parameter validation
public ValidationResult validate(String token) {
// Defensive programming at API boundary
Objects.requireNonNull(token, "token must not be null");
// Implementation ensures non-null return
if (isValid(token)) {
return ValidationResult.valid();
}
return ValidationResult.invalid("Token validation failed");
}
Nullable Parameters
Use @Nullable sparingly for parameters that genuinely accept null:
// Good - null has clear meaning (use default)
public String format(@Nullable Locale locale) {
Locale effectiveLocale = locale != null ? locale : Locale.getDefault();
return formatter.format(effectiveLocale);
}
// Best - overload methods instead of nullable parameters
public String format() {
return format(Locale.getDefault());
}
public String format(Locale locale) {
return formatter.format(locale);
}
Collections and Generics
Apply null-safety to collection types:
// With @NullMarked, all elements are non-null
public List<User> getActiveUsers() {
// Returns non-null list of non-null User objects
return users.stream()
.filter(User::isActive)
.toList();
}
// Use @Nullable for nullable elements
public List<@Nullable String> getOptionalValues() {
// List is non-null, but elements can be null
return Arrays.asList("value1", null, "value3");
}
Unit Testing
Test that non-nullable methods never return null under any valid input conditions:
@Test
void shouldNeverReturnNull() {
// With @NullMarked, non-nullable methods must never return null
assertNotNull(service.processToken("valid"));
assertNotNull(service.processToken(""));
// Non-nullable methods should handle edge cases without returning null
assertNotNull(service.processToken("edge-case"));
}
@Test
void shouldUseOptionalForMissingValues() {
// Use Optional.empty() instead of null returns
assertTrue(service.findUser("unknown").isEmpty());
assertTrue(service.findUser("existing").isPresent());
}
@Test
void shouldRejectNullParameters() {
// Non-nullable parameters should be validated
assertThrows(NullPointerException.class,
() -> service.processToken(null));
}
Migration Strategy
For New Code
- Add
@NullMarkedtopackage-info.java - Write code assuming non-null by default
- Use
@Nullableonly where null is explicitly allowed - Use
Optional<T>for "no result" return types - Validate with unit tests
For Existing Code
- Add
@NullMarkedto package - Review all public APIs
- Add
@Nullablewhere null is currently accepted/returned - Refactor nullable returns to Optional where appropriate
- Add null checks with
Objects.requireNonNull()at API boundaries - Update tests to verify null-safety contracts
Quality Checklist
- Package has @NullMarked in package-info.java
- No @Nullable used for return types (use Optional instead)
- Nullable parameters documented and justified
- Defensive null checks at API boundaries
- Unit tests verify non-null contracts (see
pm-dev-java:junit-coreskill) - Static analysis configured and passing
- JavaDoc documents null-safety contract
- Collections specify element nullability if needed
Related Skills
pm-dev-java:java-core- Core Java patternspm-dev-java:java-lombok- Lombok patterns (interop with null safety)
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です


