Popular Searches
Popular Course Categories
Popular Courses

Understanding Exception Handling and Generics in Kotlin

Understanding Exception Handling and Generics in Kotlin

Object-Oriented Kotlin and Advanced Language Features

Understanding Exception Handling and Generics in Kotlin

Course: Android App Development

Topic: Understanding Exception Handling and Generics in Kotlin

Course Link: Android App Development Training – JustAcademy

Demo Link: Register for a Course Demo

JustAcademy Homepage: Visit JustAcademy


1. Introduction

Exception handling and generics are important Kotlin concepts that help developers build reliable, reusable, and maintainable Android applications. Exception handling allows a program to respond to runtime problems such as invalid input, failed network requests, file access errors, and parsing failures. Generics allow classes, interfaces, and functions to work with different data types while preserving compile-time type safety.

In Android development, these concepts often work together. For example, a generic repository may return a typed result containing either successful data or an error. This approach helps prevent crashes, reduces duplicated code, and makes application logic easier to test.

Learning Objectives

  • Understand exceptions and how Kotlin handles runtime errors.
  • Use try, catch, and finally correctly.
  • Understand the purpose of throw and custom exceptions.
  • Distinguish recoverable errors from programming mistakes.
  • Understand generic classes, functions, interfaces, and type parameters.
  • Use generic constraints, variance, and star projections at a beginner-to-intermediate level.
  • Apply exception handling and generics to practical Android application scenarios.

2. Understanding Exceptions in Kotlin

An exception is an object representing an unusual condition that interrupts the normal flow of a program. An exception may occur when an application attempts an invalid operation, receives unexpected data, or encounters a failure while communicating with an external system.

Common Examples

  • Converting non-numeric text into an integer.
  • Accessing a list using an invalid index.
  • Parsing malformed JSON data.
  • Reading a file that does not exist or cannot be accessed.
  • Performing an operation that requires a non-null value when the value is null.
  • Handling a network request that fails or returns an unexpected response.

Important Kotlin Exception Types

Exception Typical Cause Possible Response
NumberFormatException Invalid numeric text Validate input or use a safe conversion method.
IndexOutOfBoundsException Invalid list or array index Check collection size and index boundaries.
NullPointerException Invalid null-related operation, often involving Java interoperability or unsafe null assertions Use null-safe Kotlin operations and validate external data.
IllegalArgumentException An argument does not meet a function's requirements Validate arguments and provide meaningful error messages.
IllegalStateException An operation is attempted in an invalid state Check the object's state before performing the operation.
IOException Input/output operation failure Handle file or stream failures appropriately.

Kotlin uses Java's exception hierarchy on the JVM. Most exceptions developers encounter derive from Throwable, with Exception and Error as major branches. Applications generally handle expected operational exceptions rather than attempting to recover from every possible error.

3. The try-catch Structure

The try block contains code that may fail. If an exception of a matching type is thrown, control moves to the relevant catch block. If no exception occurs, the catch block is skipped.

Basic Syntax

try {
    val result = 10 / 0
    println(result)
} catch (exception: ArithmeticException) {
    println("The calculation could not be completed.")
}

Integer division by zero on the JVM throws an ArithmeticException. The example catches that exception and allows the program to continue.

Handling Invalid User Input

fun parseAge(input: String): Int? {
    return try {
        input.toInt()
    } catch (exception: NumberFormatException) {
        null
    }
}

fun main() {
    val age = parseAge("twenty")

    if (age == null) {
        println("Please enter a valid whole number.")
    } else {
        println("Age: $age")
    }
}

The function returns an integer when conversion succeeds and null when the text cannot be converted. For simple numeric conversions, Kotlin's toIntOrNull() is usually more concise:

val age = "25".toIntOrNull()

if (age == null) {
    println("Invalid age")
} else {
    println("Age: $age")
}

Best Practices for try-catch

  • Catch the exception types you can meaningfully handle.
  • Keep the try block focused on operations that may fail.
  • Provide a useful fallback, retry strategy, validation message, or error state.
  • Avoid silently ignoring exceptions.
  • Do not use exceptions as a replacement for ordinary conditional checks.

4. Using Multiple catch Blocks

Different failures may require different responses. Kotlin permits multiple catch blocks, and the first compatible block handles the thrown exception. Place more specific exception types before broader types.

fun divideText(first: String, second: String): Int? {
    return try {
        val numerator = first.toInt()
        val denominator = second.toInt()
        numerator / denominator
    } catch (exception: NumberFormatException) {
        println("Enter valid whole numbers.")
        null
    } catch (exception: ArithmeticException) {
        println("The denominator must not be zero.")
        null
    }
}

Here, invalid numeric input and division by zero are handled separately. This makes the response clearer and helps users understand what they need to correct.

5. Understanding finally

The finally block runs after the try and any applicable catch block in normal control flow, whether or not an exception occurred. It is commonly associated with cleanup operations.

fun performOperation() {
    try {
        println("Starting operation")
        // Perform an operation that may fail.
    } catch (exception: Exception) {
        println("Operation failed: ${exception.message}")
    } finally {
        println("Operation finished")
    }
}

Important: Kotlin does not require a finally block after every try. For resources such as streams or readers, Kotlin's use function is generally preferred because it closes the resource automatically.

Using use for Resource Management

import java.io.File

fun readTextFile(path: String): String? {
    return try {
        File(path).bufferedReader().use { reader ->
            reader.readText()
        }
    } catch (exception: java.io.IOException) {
        println("Unable to read the file: ${exception.message}")
        null
    }
}

The use function closes the resource after the block completes, including when an exception occurs. This is particularly important when working with files, streams, and other closeable resources.

6. Throwing Exceptions with throw

The throw keyword explicitly raises an exception. It is useful when a function detects an invalid argument, an unsupported operation, or a state that prevents it from continuing safely.

fun calculateDiscount(price: Double, percentage: Double): Double {
    require(price >= 0) { "Price cannot be negative." }
    require(percentage in 0.0..100.0) {
        "Discount must be between 0 and 100."
    }

    return price - (price * percentage / 100)
}

The require function throws an IllegalArgumentException when its condition is false. Use it to validate input arguments. For invalid object or application state, consider check, which throws an IllegalStateException.

Using throw Directly

fun getProductPrice(price: Double): Double {
    if (price < 0) {
        throw IllegalArgumentException("Price cannot be negative.")
    }
    return price
}

In this example, the function refuses to accept a negative price. Throwing an exception is appropriate because the argument violates the function's contract.

7. Creating Custom Exceptions

A custom exception gives a specific name to an application-level failure. It can make business rules easier to understand, particularly when an application has domain-specific validation or processing requirements.

class InvalidAgeException(message: String) : Exception(message)

fun registerUser(age: Int) {
    if (age < 18) {
        throw InvalidAgeException("User must be at least 18 years old.")
    }

    println("Registration successful")
}

fun main() {
    try {
        registerUser(16)
    } catch (exception: InvalidAgeException) {
        println(exception.message)
    }
}

Choosing the Exception Base Class

Base Class Meaning When to Consider It
Exception Represents an exception that an application may handle. Use when a custom exception represents a recoverable or reportable application failure.
RuntimeException A runtime exception type in the Java exception hierarchy. Use when the exception represents an unchecked programming or domain failure and this choice fits the API design.

Kotlin does not enforce checked exceptions at compile time. You are not required to declare that a function throws a particular exception, although documentation and annotations may still communicate exception behavior to Java callers.

8. Exceptions in Android Applications

Android apps interact with networks, databases, local storage, permissions, user input, and lifecycle events. Each of these areas can produce failures. Good exception handling should turn expected failures into useful application states rather than allowing an avoidable crash.

Example: Safely Parsing a Value

fun calculateTotal(priceText: String, quantityText: String): Double? {
    val price = priceText.toDoubleOrNull() ?: return null
    val quantity = quantityText.toIntOrNull() ?: return null

    if (price < 0 || quantity < 0) {
        return null
    }

    return price * quantity
}

This example uses safe conversion rather than throwing and catching an exception for ordinary invalid form input. An Android screen can display a validation message when the function returns null.

Example: Handling a Repository Failure

suspend fun loadProfile(): Profile? {
    return try {
        profileRepository.fetchProfile()
    } catch (exception: java.io.IOException) {
        // Log diagnostic details through the app's logging system.
        // The UI should display a suitable network error state.
        null
    }
}

Note: This is a simplified example. A production repository should distinguish a legitimate missing profile from a network or server failure rather than using null for every outcome. Avoid catching broad exceptions without a clear reason, and do not expose internal exception details directly to end users.

Android Exception-Handling Guidelines

  • Handle expected network, storage, and parsing failures near the appropriate application boundary.
  • Use coroutines and their structured cancellation behavior correctly; do not accidentally swallow CancellationException.
  • Keep detailed diagnostics in logs or monitoring tools while showing clear messages to users.
  • Use validation for predictable user-input errors instead of relying on exceptions.
  • Do not catch Throwable indiscriminately because it includes serious errors that applications generally should not attempt to recover from.

9. Introduction to Generics in Kotlin

Generics allow a class, interface, or function to work with a type specified by its caller. Instead of writing separate implementations for strings, integers, and other types, a generic implementation can work with all supported types while retaining type safety.

For example, a generic container can store a value of type String, Int, or a custom class such as User. The compiler checks how that container is used, helping prevent incompatible values from being passed or returned.

Why Generics Are Useful

  • Reduce repeated code across different data types.
  • Improve compile-time type safety.
  • Reduce unnecessary casts.
  • Create reusable collections, repositories, adapters, and utility functions.
  • Make API contracts clearer by specifying the expected input and output types.

10. Generic Classes

A generic class declares a type parameter inside angle brackets. The parameter is commonly named T, although any meaningful valid identifier can be used.

class Box<T>(val value: T)

fun main() {
    val nameBox = Box("Kotlin")
    val numberBox = Box(100)

    println(nameBox.value)
    println(numberBox.value)
}

Box<String> stores a string, while Box<Int> stores an integer. Kotlin can usually infer the type from the constructor argument, so you do not need to write the type explicitly in these examples.

Multiple Type Parameters

class PairData<A, B>(
    val first: A,
    val second: B
)

fun main() {
    val userRecord = PairData("User-101", 28)
    println(userRecord.first)
    println(userRecord.second)
}

The class uses two independent type parameters. This is useful when a data structure must associate values of different types, such as an identifier and a result, or a key and a value.

11. Generic Functions

A generic function declares its type parameter before the function name or signature. It can then accept and return values of that type without losing information about the type.

fun <T> getFirstItem(items: List<T>): T? {
    return items.firstOrNull()
}

fun main() {
    val firstName = getFirstItem(listOf("Asha", "Ravi"))
    val firstNumber = getFirstItem(listOf(10, 20, 30))

    println(firstName)
    println(firstNumber)
}

The same function works with a list of strings and a list of integers. Its return type is T? because an empty list has no first item, so the function may return null.

Generic Utility Function Example

fun <T> printItems(items: List<T>) {
    for (item in items) {
        println(item)
    }
}

fun main() {
    printItems(listOf("Android", "Kotlin", "Jetpack"))
    printItems(listOf(1, 2, 3, 4))
}

This function accepts lists of different element types without requiring separate versions for each type.

12. Generic Interfaces

Interfaces can also declare type parameters. A generic interface defines a contract that can be implemented for specific types.

interface Repository<T> {
    fun getAll(): List<T>
    fun findById(id: String): T?
}

data class Product(
    val id: String,
    val name: String
)

class ProductRepository : Repository<Product> {
    private val products = listOf(
        Product("P1", "Keyboard"),
        Product("P2", "Mouse")
    )

    override fun getAll(): List<Product> = products

    override fun findById(id: String): Product? {
        return products.find { it.id == id }
    }
}

The Repository<Product> implementation is specifically responsible for product data. A different implementation could use Repository<User> or Repository<Order> while following the same interface contract.

13. Generic Type Constraints

Sometimes a generic function should accept only types that meet a particular requirement. Kotlin supports upper bounds to restrict the permitted type arguments.

Using a Single Upper Bound

fun <T : Number> doubleValue(value: T): Double {
    return value.toDouble() * 2
}

fun main() {
    println(doubleValue(10))
    println(doubleValue(5.5))
}

The constraint T : Number means that the supplied type must be a subtype of Number. This allows the function to call toDouble() safely.

Using where for Multiple Constraints

fun <T> compareItems(first: T, second: T): Boolean
    where T : CharSequence,
          T : Comparable<T> {
    return first.compareTo(second) == 0
}

The where clause allows a type parameter to satisfy multiple constraints. Use this when a generic operation depends on more than one capability.

14. Variance: out and in

Variance describes how generic types relate when their type arguments have an inheritance relationship. Kotlin provides out and in to express safe producer and consumer behavior.

Keyword Meaning Typical Use
out T Covariant type parameter; the type is produced as output. Read-only data providers and result sources.
in T Contravariant type parameter; the type is consumed as input. Handlers, processors, and comparators.
No variance modifier Invariant type parameter by default. Types that both consume and produce the parameter type.

Covariance with out

open class Animal
class Dog : Animal()

interface Producer<out T> {
    fun produce(): T
}

class DogProducer : Producer<Dog> {
    override fun produce(): Dog = Dog()
}

fun acceptAnimalProducer(producer: Producer<Animal>) {
    val animal = producer.produce()
}

Because Producer only produces values of type T, it can declare out T. A producer of Dog can then be used where a producer of Animal is expected.

Contravariance with in

open class Animal
class Dog : Animal()

interface Handler<in T> {
    fun handle(value: T)
}

class AnimalHandler : Handler<Animal> {
    override fun handle(value: Animal) {
        println("Handling an animal")
    }
}

fun acceptDogHandler(handler: Handler<Dog>) {
    handler.handle(Dog())
}

A handler that accepts any Animal can also handle a Dog. The in modifier expresses this consumer relationship.

15. Star Projections and Type Safety

A star projection (*) represents a generic type when the exact type argument is unknown. It is useful when code needs to work with a generic value without assuming a particular type.

fun printListInfo(items: List<*>) {
    println("Number of items: ${items.size}")

    for (item in items) {
        println(item)
    }
}

The function can accept lists of different types. Because the element type is unknown, code should not attempt to add arbitrary values to a mutable generic collection through a star-projected reference.

16. Combining Generics and Exception Handling

Generics and exception handling can be combined to create reusable operations that return either a successful value or an error. Kotlin's standard library includes Result<T>, which represents a successful result containing a value of type T or a failure containing an exception.

Using Result

fun parseNumber(input: String): Result<Int> {
    return runCatching {
        input.toInt()
    }
}

fun main() {
    val result = parseNumber("123")

    result
        .onSuccess { number ->
            println("Parsed value: $number")
        }
        .onFailure { exception ->
            println("Could not parse the number.")
        }
}

The function returns Result<Int>, so the successful value is known to be an integer. A failure carries the exception. In production code, inspect and report failures appropriately rather than displaying internal exception messages directly to users.

Important: runCatching catches any Throwable, including exceptions that may be associated with cancellation or serious runtime problems. In coroutine-based Android code, take care not to convert a CancellationException into an ordinary failure. For operations where only specific exceptions are expected, a targeted try-catch may be more appropriate.

A Generic Result Wrapper

sealed class AppResult<out T> {
    data class Success<T>(val data: T) : AppResult<T>()
    data class Error(val message: String) : AppResult<Nothing>()
    data object Loading : AppResult<Nothing>()
}

fun loadUsername(): AppResult<String> {
    return try {
        AppResult.Success("Aarav")
    } catch (exception: Exception) {
        AppResult.Error("Unable to load username")
    }
}

This simplified sealed class models three possible states: success, error, and loading. The out T declaration makes the result type covariant, while Nothing allows the error and loading states to fit a result type whose successful data type is not known.

In a real Android project, the loading state is often managed by the ViewModel, while repository functions return a success/error result or use a clearly documented exception strategy. Avoid catching exceptions that cannot be meaningfully recovered from.

17. Practical Android Example: Generic Safe Parser

A generic parsing utility can accept a conversion function and return a typed result. This is useful when several input types require the same general error-handling pattern.

fun <T> safelyParse(
    input: String,
    parser: (String) -> T
): Result<T> {
    return try {
        Result.success(parser(input))
    } catch (exception: IllegalArgumentException) {
        Result.failure(exception)
    }
}

fun main() {
    val ageResult = safelyParse("25") { value ->
        value.toInt()
    }

    val priceResult = safelyParse("199.99") { value ->
        value.toDouble()
    }

    println(ageResult.getOrNull())
    println(priceResult.getOrNull())
}

This utility can work with different output types because T is inferred from the parser. However, the example only handles IllegalArgumentException. If the parser may throw other documented exception types, handle them deliberately or allow them to propagate. Avoid catching all exceptions without a clear recovery strategy.

18. Common Mistakes and Troubleshooting

Problem Why It Happens Recommended Solution
Using a broad catch for every error Unrelated failures are hidden or incorrectly treated as recoverable. Catch specific exceptions that the code can handle meaningfully.
Returning null for every failure The caller cannot distinguish missing data from network or parsing errors. Use a typed result, sealed class, or documented exception strategy.
Ignoring a caught exception The application loses useful diagnostic information. Log appropriate diagnostic details and provide a suitable response.
Using the wrong generic type Type arguments do not match the function or class contract. Check the declared type parameter and inferred types.
Attempting unsafe casts The actual runtime type may not match the assumed type. Prefer generic APIs that preserve type information.
Incorrect variance declaration A type parameter is used in a position prohibited by in or out. Review whether the type produces or consumes values of the parameter type.
Swallowing coroutine cancellation A broad catch or runCatching converts cancellation into a normal failure. Propagate cancellation when handling coroutine failures.
Leaving resources open Cleanup is skipped after an exception. Use use for closeable resources where supported.

19. Best Practices

  • Use safe conversion functions such as toIntOrNull() when invalid input is expected.
  • Use require and check to communicate invalid arguments and invalid states.
  • Catch only failures that the current layer can handle or translate meaningfully.
  • Use custom exception types when they improve clarity for a genuine application-specific failure.
  • Prefer generic classes and functions over duplicated implementations for different data types.
  • Choose meaningful generic parameter names when the type's role is not obvious.
  • Use upper bounds to enforce necessary capabilities instead of relying on unsafe casts.
  • Apply in and out only when the type's usage supports the declared variance.
  • Use typed result models when callers need to distinguish success, loading, and failure states.
  • Write unit tests for successful operations, invalid inputs, and expected failures.

20. Practice Exercises

  1. Write a function that converts a string to an integer and reports invalid input without crashing.
  2. Create a custom InvalidEmailException and use it to validate an email address.
  3. Write a generic Container<T> class that stores and returns a value.
  4. Create a generic function that returns the last element of a list or null when the list is empty.
  5. Build a generic repository interface and implement it for a Product data class.
  6. Write a function constrained to numeric types that returns the numeric value as a Double.
  7. Model a loading, success, and error state with a sealed generic class.
  8. Build a small Android form-validation example that combines safe input conversion with a typed result.

21. Mini Project: Generic User Registration Validator

Project goal: Create a reusable validation layer for a simple registration screen. The project should validate a username and age, return meaningful errors, and avoid crashes when users enter invalid data.

Requirements

  • Create a User data class with a username and age.
  • Use a generic result type to represent success or failure.
  • Use safe conversion to parse the age from text.
  • Reject blank usernames and invalid age values.
  • Return a user object on success and a clear validation message on failure.
  • Test valid data, blank usernames, non-numeric ages, and negative ages.

Sample Implementation

data class User(
    val username: String,
    val age: Int
)

sealed class ValidationResult<out T> {
    data class Success<T>(val value: T) : ValidationResult<T>()
    data class Failure(val message: String) : ValidationResult<Nothing>()
}

fun validateUser(
    username: String,
    ageText: String
): ValidationResult<User> {
    if (username.isBlank()) {
        return ValidationResult.Failure("Username is required.")
    }

    val age = ageText.toIntOrNull()
        ?: return ValidationResult.Failure("Enter a valid age.")

    if (age < 0) {
        return ValidationResult.Failure("Age cannot be negative.")
    }

    return ValidationResult.Success(User(username.trim(), age))
}

This project demonstrates that not every validation problem requires throwing an exception. Expected user-input errors can be represented as typed results, while exceptions remain available for unexpected operational failures. In an Android app, the UI can display the failure message or navigate forward when validation succeeds.

Testing the Mini Project

fun main() {
    val validResult = validateUser("Aarav", "22")
    val invalidNameResult = validateUser("", "22")
    val invalidAgeResult = validateUser("Aarav", "abc")

    when (validResult) {
        is ValidationResult.Success ->
            println("Registered: ${validResult.value.username}")
        is ValidationResult.Failure ->
            println("Error: ${validResult.message}")
    }

    when (invalidNameResult) {
        is ValidationResult.Success -> println("Unexpected success")
        is ValidationResult.Failure -> println(invalidNameResult.message)
    }

    when (invalidAgeResult) {
        is ValidationResult.Success -> println("Unexpected success")
        is ValidationResult.Failure -> println(invalidAgeResult.message)
    }
}

Run the tests with valid and invalid values. Confirm that successful input returns a User object and invalid input returns an appropriate failure message without crashing the application.

22. Interview Questions and Answers

Q1. What is exception handling in Kotlin?

Exception handling is the process of responding to exceptional runtime conditions using mechanisms such as try, catch, finally, and throw.

Q2. Does Kotlin support checked exceptions?

Kotlin treats exceptions as unchecked by default. Functions do not have to declare or catch exceptions at compile time, although documenting possible failures remains good practice.

Q3. What is the difference between throw and throws?

throw is a Kotlin keyword used to raise an exception. Kotlin does not require a Java-style throws declaration, but the @Throws annotation can be used when communicating exception behavior to Java or other interoperable callers.

Q4. What is the purpose of finally?

The finally block performs cleanup or other required final operations after the try and catch flow. For closeable resources, Kotlin's use function is often the more convenient option.

Q5. What is a custom exception?

A custom exception is an application-defined exception class used to represent a specific failure, such as invalid registration data or a domain-specific operation error.

Q6. What are generics in Kotlin?

Generics allow classes, functions, and interfaces to work with type parameters so that code can be reused across multiple data types while preserving type safety.

Q7. What does the T type parameter represent?

T is a conventional name for a generic type parameter. It represents a type supplied by the caller or inferred by the compiler.

Q8. What is a generic function?

A generic function declares one or more type parameters and uses them in its parameters, return type, or implementation. For example, fun <T> first(items: List<T>): T? can work with lists of different element types.

Q9. What is the purpose of an upper bound?

An upper bound restricts a generic type parameter to a particular base type or interface, allowing the function to use capabilities guaranteed by that constraint.

Q10. What is the difference between in and out?

out T expresses covariance for a type parameter used as a producer, while in T expresses contravariance for a type parameter used as a consumer.

Q11. What is a star projection?

A star projection, written as *, allows code to work with a generic type when its exact type argument is unknown, while respecting Kotlin's type-safety rules.

Q12. What is Result<T>?

Result<T> represents either a successful operation containing a value of type T or a failure containing an exception. It is useful when a function needs to return success or failure as a value.

Q13. Why should broad exception handling be avoided?

Broad exception handling may hide programming errors, intercept failures that should propagate, or interfere with coroutine cancellation. Catch the exceptions the current layer can handle meaningfully.

Q14. How are exceptions and generics useful together in Android?

Generic repositories and result wrappers can return typed data and represent errors consistently. This helps ViewModels and UI components handle loading, success, and failure states without duplicating logic for each data type.

Q15. Should every validation error throw an exception?

No. Expected user-input errors are often better handled with validation results, nullable conversions, or field-level error messages. Exceptions are more appropriate when an operation cannot proceed according to its contract or an unexpected operational failure occurs.

23. Quick Revision Checklist

  • Understand the relationship between Throwable, Exception, and Error.
  • Know how try, catch, finally, and throw work.
  • Use require for invalid arguments and check for invalid states.
  • Use safe conversions for expected invalid input.
  • Create custom exceptions when they make application errors more meaningful.
  • Use use to manage supported closeable resources safely.
  • Understand generic classes, functions, interfaces, and type inference.
  • Apply upper bounds and the where clause when generic operations require specific capabilities.
  • Understand in, out, and star projections.
  • Combine typed results with appropriate error handling in application and repository layers.
  • Test normal operation, invalid input, expected failures, and edge cases.

24. Learning Resources

25. Conclusion

Exception handling and generics are foundational Kotlin skills for building dependable Android applications. Exception handling helps applications respond to failures deliberately, while generics make reusable components safer and easier to maintain. By applying specific exception handling, safe input conversion, custom exceptions where appropriate, generic repositories, and typed result models, developers can build application logic that is clearer, more testable, and more resilient.

Practice these concepts with small Kotlin programs first, then apply them to Android forms, repository layers, API responses, and application state management. Understanding both concepts will help you write cleaner Kotlin code and prepare for practical Android development tasks and technical interviews.

Previous Next
whatsapp