How do I use !! and avoid NullPointerException in Kotlin?

In Kotlin, the !! operator is called the not-null assertion operator. It is used to tell the compiler that a null value will never be encountered for a given variable. If Kotlin encounters a null during runtime while using !!, it will throw a NullPointerException (NPE).

While using !! can be convenient in some cases, it is generally discouraged because:

  • It defeats Kotlin’s strong null-safety system.
  • It can lead to unexpected crashes if null values occur.

Example of !! usage:

val name: String? = getNullableName() // Can be null
val length = name!!.length // Throws NullPointerException if `name` is null.

If name happens to be null, the code will throw a KotlinNullPointerException.

Strategies to Avoid NullPointerException Without !!

Instead of using !!, you should take advantage of Kotlin’s powerful null-safety features:

1. Safe Call Operator (?.)

The ?. operator allows you to safely access properties or call methods on nullable objects. If the object is null, the operation is skipped, and null is returned.

val name: String? = getNullableName()
val length = name?.length // If `name` is null, `length` will also be null

2. Elvis Operator (?:)

The Elvis operator allows you to provide a default value if a nullable object is null.

val name: String? = getNullableName()
val length = name?.length ?: 0 // If `name` is null, default to `0`.

3. Null-Check with if Statements

You can perform explicit null checks using conditional statements.

val name: String? = getNullableName()
if (name != null) {
    val length = name.length
    println("Length: $length")
} else {
    println("Name is null")
}

4. let Extension Function

The let function is a great way to perform operations safely on a nullable value only if it is not null.

val name: String? = getNullableName()
name?.let {
    println("Length: ${it.length}")
} ?: println("Name is null")

5. Smart Casts

Kotlin’s type system can automatically cast a nullable value to a non-nullable type after a null check.

val name: String? = getNullableName()
if (name != null) {
    println("Length: ${name.length}") // Smart cast to non-nullable `String`.
}

6. Using Default Value in Functions

You can provide default values for parameters in functions to ensure they’re never null.

fun greet(name: String = "Guest") {
    println("Hello, $name!")
}

greet(null) // Compiler error
greet()     // Uses default value: "Hello, Guest!"

7. Require Non-Null Parameters

If a parameter cannot be null, declare it as a non-nullable type (String instead of String?). This way, Kotlin ensures null values are not passed to such parameters.

When to Use !!

Use !! only when you’re absolutely certain that the value is never null, and a null value would indicate a programmer error or an exceptional situation.
For example:

val bundle: Bundle = intent.extras!!
val value = bundle.getString("key")!!

In this case, you’re assuming that intent.extras and the value for the key "key" will always exist. However, it’s better to avoid such assumptions and handle null-safety appropriately.

Summary

  • Avoid !! as much as possible.
  • Use Kotlin’s built-in null-safety features like ?., ?:, if, let, and smart casts.
  • Reserve !! for situations where you’re certain about non-nullability (or avoid it altogether). Writing safer and more reliable code is one of Kotlin’s strong suits

How do I use smart casts in Kotlin control flow?

In Kotlin, smart casts are a feature that allows the compiler to automatically cast an object to a target type within a certain scope if it determines that the cast is safe. Smart casts are most commonly used in control flow statements, such as if, when, and loops.

Here’s how you can use smart casts in Kotlin’s control flow:


1. Using if Statements

The is operator is used to check if a value is of a particular type. If the condition is true, Kotlin smart casts the variable to that type within the scope of the condition.

fun describe(obj: Any): String {
    return if (obj is String) {
        // obj is automatically cast to String in this block
        "The length of the string is ${obj.length}"
    } else if (obj is Int) {
        // obj is automatically cast to Int in this block
        "The number is $obj"
    } else {
        "Unknown type"
    }
}

2. Using when Expressions

The when expression is great for smart casting. It automatically casts the variable to the type you check for using the is operator.

fun analyze(input: Any): String {
    return when (input) {
        is String -> "This is a String of length ${input.length}"  
        is Int -> "This is an Integer: ${input + 1}"              
        is Boolean -> "A boolean value: $input"                  
        else -> "Unsupported type"
    }
}

3. !is to Exclude a Type

You can use !is to exclude a specific type. Smart casts still work because excluding one type helps Kotlin infer the possible remaining types.

fun printNumberIfNotString(value: Any) {
    if (value !is String) {
        // value is NOT a String, so it can be treated as something else
        println("This value is not a string and is: $value")
    } else {
        println("Actually, this is a String: $value")
    }
}

4. Smart Casts in Loops

Smart casts can also be used in loops, typically in for loops when iterating over collections. For types with mixed content, you can use smart casts to handle specific elements dynamically.

fun handleList(items: List<Any>) {
    for (item in items) {
        when (item) {
            is String -> println("String of length ${item.length}")
            is Int -> println("Integer: $item")
            is Boolean -> println("Boolean: $item")
            else -> println("Unknown type")
        }
    }
}

5. Combining with Null Checks

Smart casts are also applicable with nullable types. After a null check (!= null), the variable will be smart cast to the non-nullable type.

fun printNonNullLength(text: String?) {
    if (text != null) {
        // text is smart cast to a non-nullable String
        println("The length of the string is ${text.length}")
    }
}

Alternatively, you can use let for null-safe operations:

fun printNonNullLengthUsingLet(text: String?) {
    text?.let {
        // 'it' is the non-null value
        println("The length of the string is ${it.length}")
    }
}

Keynotes:

  • Smart casts work only with immutable (val) variables or local variables whose value cannot be changed.
  • If the cast can’t be determined at compile time, you’ll need to explicitly cast using as or other constructs.

For safe casting, use as?:

val obj: Any = "String"
val safeString: String? = obj as? String  // Returns null if the cast is not possible

Smart casts make Kotlin concise and readable by eliminating the need for manual casting, while ensuring type safety at compile time!

How to use the new API enhancements in java.nio.file in Java 17

Java 17 introduced several significant enhancements in the java.nio.file package, focusing on improving file system operations, security, and performance. Below is an explanation of the new APIs and available enhancements, with examples demonstrating how to use them.

Key API Enhancements in java.nio.file for Java 17

1. Files.mismatch()

The method Files.mismatch(Path, Path) was added to efficiently compare two files. It helps identify the position where two files differ or returns -1 if the files are identical.

Example:

package org.kodejava.nio;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class FilesMismatchExample {
    public static void main(String[] args) throws IOException {
        Path file1 = Path.of("file1.txt");
        Path file2 = Path.of("file2.txt");

        // Create sample files
        Files.writeString(file1, "Hello, world!");
        Files.writeString(file2, "Hello, Java!");

        long mismatchPosition = Files.mismatch(file1, file2);

        if (mismatchPosition == -1) {
            System.out.println("Files are identical.");
        } else {
            System.out.println("Files differ beginning at byte position: " + mismatchPosition);
        }
    }
}

Usage Notes:

  • This method is especially useful for large files where reading and comparing the entire contents manually would be inefficient.
  • For identical files, the method returns -1.

2. Files.copy() Enhancements

The Files.copy(InputStream in, Path target, CopyOption... options) method now supports the StandardCopyOption.REPLACE_EXISTING option to overwrite existing files directly.

Example:

package org.kodejava.nio;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class FilesCopyExample {
    public static void main(String[] args) throws Exception {
        Path targetPath = Path.of("output.txt");

        try (InputStream inputStream = new ByteArrayInputStream("File content".getBytes())) {
            Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
        System.out.println("File copied successfully to: " + targetPath);
    }
}

Usage Notes:

  • Prior to Java 17, replacing existing files required explicitly deleting the file first.
  • This enhancement simplifies file replacement logic.

3. Support for Hidden Files in Files.isHidden()

Java 17 improves the handling of hidden files for certain platforms where determining this attribute was inconsistent (e.g., macOS and Linux).

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;

public class HiddenFileExample {
    public static void main(String[] args) throws Exception {
        Path filePath = Path.of(".hiddenFile");
        Files.createFile(filePath);

        if (Files.isHidden(filePath)) {
            System.out.println(filePath + " is a hidden file.");
        } else {
            System.out.println(filePath + " is not a hidden file.");
        }
    }
}

4. File Permission Enhancements on Unix-like Systems

Java 17 improves security and performance for managing file permissions using PosixFilePermissions.

Example:

package org.kodejava.nio;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;

public class FilePermissionExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("example.txt");
        Files.createFile(path);

        Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rw-r--r--");
        Files.setPosixFilePermissions(path, permissions);

        System.out.println("File permissions: " + Files.getPosixFilePermissions(path));
    }
}

Usage Note:

  • This improvement provides more robust support for file permissions on Unix-like operating systems.

Summary Table of Changes

Enhancement Description Java Version
Files.mismatch() Compares two files to find the first mismatch position or confirms equality Java 17
Enhanced Files.copy() Overwrite files without manually deleting them Java 17
Improved Files.isHidden() Better cross-platform handling of hidden files Java 17
File Permission Enhancements Improved security and performance on Unix-like systems Java 17

These enhancements improve efficiency, accessibility, and usability when working with file system operations. You can start using them to simplify your file-handling logic in Java applications.

How to compile and run Java 17 code using command line

To compile and run Java 17 code using the command line, follow these steps:


1. Install Java 17

  • Ensure that Java 17 is installed on your system.
  • Run the following command to check the installed Java version:
java -version

If Java 17 is not installed, download and install it from the official Oracle website or use OpenJDK.


2. Write Your Java Code

  • Create a Java file with the .java extension. For example, create a file named HelloWorld.java with the following content:
public class HelloWorld {
   public static void main(String[] args) {
       System.out.println("Hello, World!");
   }
}

3. Open Command Line

  • Open a terminal (on Linux/Mac) or Command Prompt/PowerShell (on Windows).

4. Navigate to the Directory

  • Go to the directory where the .java file is located using the cd command. For example:
cd /path/to/your/code

5. Compile the Java File

  • Use the javac command to compile the .java file into bytecode. The javac compiler will create a .class file.
javac HelloWorld.java
  • If there are no errors, you’ll see a file named HelloWorld.class in your directory.

6. Run the Compiled Java File

  • Execute the compiled .class file using the java command (without the .class extension):
java HelloWorld
  • You should see the following output:
Hello, World!

7. (Optional) Use Java 17 Specific Features

  • Java 17 brought several new features such as sealed classes, pattern matching for switch, and more. Make sure your code uses features specific to Java 17 to fully utilize it.

Common Troubleshooting

  1. 'javac' is not recognized as an internal or external command:
    • Ensure Java is added to your system’s PATH environment variable. Refer to your operating system’s documentation to add the Java bin directory to the PATH.
  2. Specify Java Version (if multiple versions are installed):
    • Use the full path to the desired Java version for compilation and execution:
/path/to/java17/bin/javac HelloWorld.java
/path/to/java17/bin/java HelloWorld

With these steps, your Java 17 code should successfully compile and run from the command line.

How to use helpful NullPointerExceptions in Java 17

In Java 14, along with the -XX:+ShowCodeDetailsInExceptionMessages feature, Helpful NullPointerExceptions were introduced. This feature provides detailed and precise messages when a NullPointerException (NPE) occurs. It is available starting from Java 14 as a preview feature and was enabled by default (no longer requiring the JVM flag) starting with Java 16. This behavior continues in Java 17.

These enhancements tell you exactly which object reference was null, making debugging easier compared to the default NPE messages.


Steps to Use Helpful NullPointerExceptions in Java 17

  1. Ensure Java 17 is Installed
    • Verify that the installed JDK version is Java 17 or newer. Use:
    java -version
    
  2. By Default, It’s Enabled
    • Starting from Java 16, Helpful NullPointerExceptions are enabled by default, so no additional JVM flag or setup is required.
  3. Run Your Application
    • If your code throws a NullPointerException, the detailed message will be generated.
  4. How It Works
    • When a NullPointerException is thrown, the JVM will now include details in the exception’s message about the null reference that caused the problem.

Example

Code Example

package org.kodejava.basic;

public class NullPointerDemo {
   public static void main(String[] args) {
      String str = null;
      System.out.println(str.toLowerCase()); // Will throw a NullPointerException
   }
}

Output

Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toLowerCase()" because "str" is null

If you use field/method chaining, the message will identify exactly which part caused the NPE.

Example with Field Access

class Person { 
    Address address; 
}

class Address { 
    String city; 
}

public class HelpfulNPEExample { 
    public static void main(String[] args) { 
        Person person = new Person(); 
        System.out.println(person.address.city); // Accessing null property
    }
}

Detailed Output

Exception in thread "main" java.lang.NullPointerException: 
Cannot read field "city" because "person.address" is null

Enabling or Disabling (Optional)

Helpful NullPointerExceptions can be disabled using the following JVM argument:

-XX:-ShowCodeDetailsInExceptionMessages

To enable explicitly (though it’s enabled by default in Java 17+):

-XX:+ShowCodeDetailsInExceptionMessages

Add this argument when running your application:

java -XX:+ShowCodeDetailsInExceptionMessages YourMainClass

Benefits of Helpful NullPointerExceptions

  1. Faster Debugging: You no longer need to search manually for which variable or reference is null.
  2. Enhanced Error Information: Pinpoints the exact null reference, which is especially useful in complex codebases.
  3. Productivity Increase: Saves time during troubleshooting and debugging.

Java 17 users benefit from this feature out-of-the-box, making it a significant enhancement for clean and error-free development.