NullPointerException is probably one of the most familiar characters in a Java developer’s day-to-day work. Almost everyone has had code blow up because of it at some point.

Take a simple example:

public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    Company company = employee.getTeam().getDepartment().getCompany();
    System.out.println(company);
}

private Employee getEmployee() {
    Employee employee = new Employee();
    employee.setEmployeeName("JiaGouWuDao");
    employee.setTeam(new Team("DevTeam4"));
    return employee;
}

Run this, and instead of getting the result you want, you may end up with a NullPointException.

Time to say goodbye to NullPointerException gracefully

As one of the most typical exceptions in Java, it is often among the first runtime errors developers meet. Over time, it turns into a kind of curse: every line of code starts to come with the question, “Do I need another null check here?” Eventually, code gets filled with defensive checks.

So the earlier example often becomes this:

public void getCompanyFromEmployee() {
    Employee employee = getEmployee();
    if (employee == null) {
        // do something here...
        return;
    }
    Team team = employee.getTeam();
    if (team == null) {
        // do something here...
        return;
    }
    Department department = team.getDepartment();
    if (department == null) {
        // do something here...
        return;
    }
    Company company = department.getCompany();
    System.out.println(company);
}

Most Java projects have code like this somewhere. Every line feels like it was written by someone who has been hurt by NullPointException before and now checks everything just to stay safe.

The defensive coding style caused by null

Why null causes so much trouble

That small example already shows several problems that come with null:

  • Null pointer exceptions make runtime behavior unreliable. One missed check and the program may crash.
  • Code becomes bloated with repetitive checks and guards, which hurts readability.

Problems introduced by null

There is another issue too:

  • null is ambiguous. If a method returns null, the caller often cannot tell whether it means the logic failed, the data is missing unexpectedly, or null is actually an acceptable business result.

A better habit is to avoid using null as much as possible, and handle “no value” differently depending on the situation:

Ways to handle null more cleanly

  • If the missing value is caused by an error in code or business logic, throw an exception so the caller is forced to notice and handle it.
  • If the absence of a value is part of normal business behavior, consider returning Optional instead.

Of course, even if your own code avoids returning null, you still have to deal with APIs written by others that may return it. Does that mean endless null checks are unavoidable? Not necessarily. There are more elegant ways to avoid falling into the same trap.

Using Optional to deal with nulls

Is Optional always safer than return null?

It is common to say that returning Optional reduces the burden on callers and helps prevent null pointer exceptions.

But is returning an Optional object automatically better than return null?

Not always. It depends entirely on how it is used.

Consider this code:

public void testCallOptional() {
    Optional optional = getContent();
    System.out.println("-------下面代码会报异常--------");
    try {
        // 【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果
        Content content = optional.get();
        System.out.println(content);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("-------上面代码会报异常--------");
}

private OptionalgetContent() {
    return Optional.ofNullable(null);
}

After running it, you will get an exception:

-------下面代码会报异常--------
java.util.NoSuchElementException: No value present
    at java.util.Optional.get(Optional.java:135)
 at com.veezean.skills.optional.OptionalService.testCallOptional(OptionalService.java:47)
 at com.veezean.skills.optional.OptionalService.main(OptionalService.java:58)
-------上面代码会报异常--------

So if calling Optional.get() directly is dangerous, should you just check first?

public void testCallOptional2() {
    Optional optional = getContent();
    // 使用前先判断下元素是否存在
    if (optional.isPresent()) {
        Content content = optional.get();
        System.out.println(content);
    }
}

Yes, this avoids the exception. But it does not really solve the deeper problem. This is not much different from returning null and checking it before use:

public void testNullReturn2() {
    Content content = getContent2();
    if (content != null) {
        System.out.println(content.getValue());
    }
}

If Optional only turns into isPresent() plus get(), the caller is not using it in a more elegant or more reliable way.

So what does proper usage look like?

Understanding Optional properly

Creating Optional objects

An Optional represents either a wrapped value of type T, or no value at all. The class provides several static factory methods:

<table> <thead> <tr> <th>Method</th> <th>Meaning</th> </tr> </thead> <tbody> <tr> <td>empty()</td> <td>Creates an empty Optional with no actual value. You can think of it as a business-level “no value.”</td> </tr> <tr> <td>of(T t)</td> <td>Wraps a given object in an Optional. The argument must not be null, or it will throw a null pointer exception.</td> </tr> <tr> <td>ofNullable(T t)</td> <td>Creates an Optional from the given value. If t is null, it behaves like empty(). If t is not null, it behaves like of(T t).</td> </tr> </tbody> </table>

Here is how they behave:

public void testCreateOptional() {
    // 使用Optional.of构造出具体对象的封装Optional对象
    System.out.println(Optional.of(new Content("111","JiaGouWuDao")));
    // 使用Optional.empty构造一个不代表任何对象的空Optional值
    System.out.println(Optional.empty());
    System.out.println(Optional.ofNullable(null));
    System.out.println(Optional.ofNullable(new Content("222","JiaGouWuDao22")));
}

Output:

Optional[Content{id='111', value='JiaGouWuDao'}]
Optional.empty
Optional.empty
Optional[Content{id='222', value='JiaGouWuDao22'}]

One important detail: passing null into of() throws a null pointer exception. In practice, ofNullable() is usually safer because it removes the need for an extra check before wrapping the value.

Prefer ofNullable when null is possible

Common Optional methods

Before looking at real use cases, it helps to know what Optional gives you:

<table> <thead> <tr> <th>Method</th> <th>Meaning</th> </tr> </thead> <tbody> <tr> <td>isPresent</td> <td>Returns true if the Optional contains a value, otherwise false.</td> </tr> <tr> <td>ifPresent</td> <td>A functional-style API. It takes a function and executes it only if the Optional contains a value.</td> </tr> <tr> <td>get</td> <td>Returns the wrapped value. If no value exists, it throws an exception instead of returning null.</td> </tr> <tr> <td>orElse</td> <td>Similar to get, but requires a default value. If the Optional is empty, it returns that default instead of throwing.</td> </tr> <tr> <td>orElseGet</td> <td>An enhanced version of orElse. Instead of a fixed default value, it takes a function and computes a fallback value only when needed.</td> </tr> <tr> <td>orElseThrow</td> <td>Similar to orElse, but throws a specified exception when no value is present.</td> </tr> <tr> <td>filter</td> <td>Checks whether the current value matches a condition. If it does, the same Optional is returned; otherwise an empty Optional is returned.</td> </tr> <tr> <td>map</td> <td>Takes a function that converts the wrapped value into another value type, then returns a new Optional containing the result. If the mapped result is null, an empty Optional is returned.</td> </tr> <tr> <td>flatMap</td> <td>Similar to map, except the function itself returns an Optional.</td> </tr> </tbody> </table>

If map and flatMap feel familiar, that is because they also appear in Stream operations. Their role is similar: convert one type into another.

map and flatMap are similar to Stream usage

For Optional, map and flatMap often produce the same final effect. The difference is mainly in what the function you pass in is expected to return.

Difference between map and flatMap

Example:

public void testMapAndFlatMap() {
    Optional userOptional = getUser();
    Optional employeeOptional = userOptional.map(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map与flatMap的区别点:此处return的是具体对象类型
        return employee;
    });
    System.out.println(employeeOptional);

    Optional employeeOptional2 = userOptional.flatMap(user -> {
        Employee employee = new Employee();
        employee.setEmployeeName(user.getUserName());
        // map与flatMap的区别点:此处return的是具体对象的Optional封装类型
        return Optional.of(employee);
    });
    System.out.println(employeeOptional2);
}

Output:

Optional[Employee(employeeName=JiaGouWuDao)]
Optional[Employee(employeeName=JiaGouWuDao)]

Where Optional is actually useful

1. Replacing long chains of null checks

Let’s go back to the nested object access from the beginning. If you want to avoid checking every level manually, Optional can make the flow much cleaner:

public void getCompanyFromEmployeeTest() {
    Employee employeeDetail = getEmployee();
    String companyName = Optional.ofNullable(employeeDetail)
            .map(employee -> employee.getTeam())
            .map(team -> team.getDepartment())
            .map(department -> department.getCompany())
            .map(company -> company.getCompanyName())
            .orElse("No Company");
    System.out.println(companyName);
}

This walks down the chain step by step with map, then finishes with orElse to provide a default value if anything is missing along the way. It is much easier on the eyes than a wall of if statements.

Good fit: cases where a value has to be retrieved through a relatively long invocation chain.

2. Fallback lookups with a default path

A common pattern in real code is “try one way first; if that does not work, fall back to another.”

For example, getting the client IP from request headers is often written like this:

public String getClientIp(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    if (!StringUtils.isEmpty(clientIp)) {
        return clientIp;
    }
    clientIp = request.getHeader("X-Real-IP");
    return clientIp;
}

With Optional, the same idea can be expressed more neatly:

public String getClientIp2(HttpServletRequest request) {
    String clientIp = request.getHeader("X-Forwarded-For");
    return Optional.ofNullable(clientIp).orElseGet(() -> request.getHeader("X-Real-IP"));
}

Good fit: scenarios where you try one source first, and if nothing is available, either return a default or execute another retrieval path.

3. Replacing method returns that may be null

Consider this service method:

public FileInfo queryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return new FileInfo(entity.getName(), entity.getFilePath(), false);
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true);
    }
    return null;
}

One branch returns null. If this is a frequently used method, then every caller must remember to add a null check. That is exactly how fragile APIs spread through a codebase.

It can be improved like this:

public OptionalqueryOssFileInfo(String fileId) {
    FileEntity entity = fileRepository.findByIdAndStatus(fileId, 0);
    if (entity != null) {
        return Optional.ofNullable(new FileInfo(entity.getName(), entity.getFilePath(), false));
    }
    FileHistoryEntity hisEntity = fileHisRepository.findByIdAndStatus(fileId, 0);
    if (hisEntity != null) {
        return Optional.ofNullable(new FileInfo(hisEntity.getName(), hisEntity.getFilePath(), true));
    }
    return Optional.empty();
}

Now the absence of a result is explicit, and callers are much less likely to step on a hidden null.

Good fit: methods whose return value may legitimately be absent.

4. Wrapping optional fields in data models

The word Optional literally means optional, so it can also be used to make that meaning explicit in a data model.

For example:

public class PostDetail {
    private String title;
    private User postUser;
    private String content;
    private Optional lastModifyTime = Optional.empty();
    private Optional attachment = Optional.empty();
}

In a forum post, fields such as title, author, and content are required. But lastModifyTime and attachment may or may not exist. Wrapping those fields in Optional makes their nature clear.

Optional fields in a data entity

This has two obvious benefits:

  • It provides a strong business-level signal that the field is optional, much like setting a column as nullable in a database schema.
  • It saves callers from dealing with raw null checks.

Good fit: optional fields in data entities or response objects.

When throwing an exception is better than returning null

Compared with returning an Optional, throwing an exception sends a much stronger message: something abnormal has happened, and the caller is expected to deal with it explicitly.

public Team getTeamInfo() throws TestException {
    Employee employee = getEmployee();
    Team team = employee.getTeam();
    if (team == null) {
        throw new TestException("team is missing");
    }
    return team;
}

This is much clearer than simply returning null. The absence of the value is no longer vague—it is being treated as an error condition.

A practical rule is simple:

  • if “no value” is a normal business possibility, use Optional
  • if “no value” means something went wrong, throw an exception

What the JDK and major frameworks tend to do

This style is not just a matter of personal preference. Many APIs in the JDK follow the same general idea: they rarely rely on raw null as a silent return value. The same is true in many well-known open-source frameworks.

For example, this method in com.sun.jmx.snmp.agent throws an exception when there is no next element instead of returning null:

public SnmpMibSubRequest nextElement() throws NoSuchElementException  {
    if (iter == 0) {
        if (handler.sublist != null) {
            iter  ;
            return hlist.getSubRequest(handler);
        }
    }
    iter   ;
    if (iter > size) throw new NoSuchElementException();
    SnmpMibSubRequest result = hlist.getSubRequest(handler,entry);
    entry  ;
    return result;
}

And in Spring, methods commonly return Optional for data that may or may not exist. For example, under org.springframework.data.jpa.repository.support:

public OptionalfindById(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    Class domainType = getDomainClass();
    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }
    LockModeType type = metadata.getLockModeType();
    Map hints = getQueryHints().withFetchGraphs(em).asMap();
    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

That pattern is worth learning from. Instead of making callers guess what null means, the API communicates intent directly.

The more often code makes absence explicit—whether through Optional or exceptions—the less likely a project is to be controlled by NullPointerException and the defensive programming habits it forces on everyone.