Author: Amit Sharma

  • Mastering the grep Command: A Guide to Efficient Search in Linux

    Mastering the grep Command: A Guide to Efficient Search in Linux

    Introduction

    Searching through files and directories is a common task in Linux, and the grep command is a powerful tool to make this process efficient. Short for Global Regular Expression Print, grep allows you to search for patterns in files and directories with incredible flexibility. This blog will explore the basics of grep, its common flags, and how to combine it with other commands for refined searches.


    Understanding the Basics of grep

    The grep command scans through text or files to find lines matching a specified pattern. By default, grep is case-sensitive and searches for partial matches unless told otherwise.


    Example: Searching for a Pattern

    Suppose you have a file named names.txt that contains a list of names. To find names starting with “Sam”, use:

    grep Sam names.txt

    This command searches the file for lines containing “Sam” and prints them.


    Common Flags for grep

    1. Case-Insensitive Search (-i)

    By default, grep is case-sensitive. To ignore case differences, use the -i flag:

    grep -i Sam names.txt

    This returns matches regardless of whether “Sam” is uppercase or lowercase, including lines where “sam” appears in the middle or end of a word.


    2. Exact Match (-w)

    To search for exact matches of a word, use the -w flag:

    grep -w Sam names.txt

    This returns only lines where “Sam” is a standalone word, ignoring partial matches.


    3. Search Across Multiple Files

    To search a pattern in multiple files:

    grep Sam *.txt

    This searches for “Sam” in all .txt files in the current directory.


    Combining grep with Other Commands

    Using grep with ls

    You can use grep to filter output from other commands using a pipe (|). For example:

    ls /bin | grep zip

    This command lists all files in the /bin directory and filters those containing the word “zip”.

    Adding Flags for Refinement

    You can refine your search with flags:

    • Ignore case sensitivity: ls /bin | grep -i zip
    • Exact match: ls /bin | grep -w zip

    Practical Examples

    1. Searching in a Log File

    Find all occurrences of the word “error” in a log file:

    grep error server.log

    2. Counting Matches

    Count the number of matches for a pattern:

    grep -c error server.log

    3. Recursive Search

    Search for a pattern in all files and subdirectories:

    grep -r "function" /path/to/directory

    4. Highlight Matches

    Highlight matching text in the output:

    grep --color Sam names.txt

    Why Use grep?

    1. Efficiency: Quickly find patterns in large files or directories.
    2. Versatility: Combine with other commands for powerful search workflows.
    3. Precision: Use flags to tailor searches to exact needs.

    Conclusion

    The grep command is an indispensable tool for anyone working in Linux. Whether you’re searching for specific text in a file, filtering command output, or refining your search with flags, grep provides the flexibility and precision needed for efficient workflows. Start practicing with the examples above to harness the full potential of this powerful command.

  • Mastering Linux I/O Redirection: A Complete Guide with Examples

    Mastering Linux I/O Redirection: A Complete Guide with Examples

    Introduction

    Linux commands follow a basic workflow: they take input, process it, and produce output. By default, the keyboard acts as the standard input device, while the screen serves as the standard output device. However, there are situations where we want to redirect input or output to other sources, such as files. This is achieved through I/O Redirection, which allows for efficient data handling and error logging in the Linux environment.

    This blog will walk you through the three types of I/O redirection in Linux: Standard Input, Standard Output, and Standard Error, complete with practical examples.


    Understanding I/O Redirection

    I/O redirection in Linux is managed through a numbering system:

    • 0: Standard Input (stdin)
    • 1: Standard Output (stdout)
    • 2: Standard Error (stderr)

    Each of these can be redirected to files or other destinations to manage workflows effectively.


    1. Standard Input (stdin)

    Standard input typically comes from the keyboard. Using the < operator, you can redirect input from a file instead.

    Example: Recording User Input

    # Create a file and store input using the cat command
    cat > input.txt

    Steps:

    1. Type some text and press Enter.
    2. Press Ctrl + D to indicate the end of the file.

    View the Contents:

    cat < input.txt

    The text you entered earlier will be displayed on the screen.


    2. Standard Output (stdout)

    By default, command output is displayed on the screen. You can redirect it to a file using the > operator.

    Example: Saving Command Output

    # Redirect the output of the ls command to a file
    ls -l > output.txt

    Steps:

    1. Execute the command. The output.txt file will be created.
    2. View the file’s content:bashCopy codeless output.txt

    3. Standard Error (stderr)

    When errors occur, they are sent to stderr by default. Using the 2> operator, you can redirect errors to a file.

    Example: Logging Errors

    # Attempt to list a non-existent directory and redirect the error
    ls -l /bin/usr > output.txt 2> error.txt

    Steps:

    1. Check the output.txt file for successful output (if any).
    2. Check the error.txt file for error messages.

    Combining stdout and stderr

    To redirect both output and error to the same file, use 2>&1.

    ls -l /bin/usr > combined.txt 2>&1

    This command saves both standard output and error messages to combined.txt.


    Advanced Examples

    Redirecting stdout and stderr Separately

    ls -l /bin/ > output.txt 2> error.txt
    • Standard output goes to output.txt.
    • Errors go to error.txt.

    Redirecting to Append

    To append output or errors to an existing file, use >>:

    ls -l >> output.txt
    ls -l /bin/usr 2>> error.txt

    Logging Both stdout and stderr Together

    ls -l /bin/ > all_logs.txt 2>&1

    Why Use Redirection?

    1. Efficient Data Handling: Redirecting stdout and stderr allows you to save outputs and errors separately for debugging and record-keeping.
    2. Automation: Redirected outputs can be used in scripts for further processing without manual intervention.
    3. Error Management: Logs help track issues without interrupting the user experience.

    Conclusion

    Linux I/O redirection is a powerful tool for managing input, output, and error streams. Whether you’re saving command outputs, debugging errors, or automating workflows, understanding how to use stdin, stdout, and stderr effectively is a crucial skill for any Linux user or administrator.

  • The Power of the Pipe (|) in Your Terminal: A Beginner’s Guide

    The Power of the Pipe (|) in Your Terminal: A Beginner’s Guide

    In the world of command-line interfaces (CLI), the pipe (|) is a game-changing operator. If you’ve ever wondered how to harness its power, this guide is for you. The pipe allows you to connect commands, creating powerful workflows with minimal effort. Let’s dive in to understand what it is, how to use it, and why it’s so important.


    What is the Pipe (|)?

    The pipe is a symbol (|) that lets you take the output of one command and use it as the input for another. Think of it as a bridge that connects two commands, enabling them to work together seamlessly.

    For example, consider the commands:

    ls -l

    This command lists files in a detailed format.

    grep .txt

    This command filters lines containing .txt.

    By using the pipe, you can combine them:

    ls -l | grep .txt

    Now, only files with .txt in their names will be displayed in a detailed list. The ls -l command’s output becomes the input for grep .txt.


    Why Use the Pipe?

    1. Efficiency: Combine commands instead of running them separately.
    2. Flexibility: Build custom workflows tailored to your needs.
    3. Power: Leverage the strengths of multiple commands simultaneously.

    Common Use Cases

    Here are some practical scenarios where the pipe proves invaluable:

    1. Viewing Specific Processes

    ps aux | grep python

    This command lists all processes and filters for those related to Python.

    2. Counting Lines in a File

    cat myfile.txt | wc -l

    This counts the number of lines in myfile.txt.

    3. Sorting and Finding Unique Entries

    cat data.txt | sort | uniq

    This sorts the data in data.txt and removes duplicate entries.


    How to Use the Pipe in Scripts

    The pipe is not limited to the terminal; you can also use it in shell scripts to automate tasks. Here’s a simple example:

    #!/bin/bash
    
    echo "Searching for log files..."
    find /var/log -type f | grep ".log"

    This script searches for log files in /var/log.


    Best Practices

    1. Keep it Simple: Combine only as many commands as necessary.
    2. Test Step-by-Step: Run each command separately before piping them together.
    3. Use with Filters: Commands like grep, sort, and awk work wonderfully with the pipe.

    Conclusion

    The pipe (|) is a powerful tool for any developer or system administrator. It allows you to create efficient, flexible, and powerful command-line workflows. With practice, you’ll find endless ways to use it in your daily tasks.

  • Abstract Classes vs Interfaces in Kotlin: Understanding the Key Differences and When to Use Them

    Abstract Classes vs Interfaces in Kotlin: Understanding the Key Differences and When to Use Them

    Abstract Classes vs Interfaces in Simple Terms

    In Kotlin, abstract classes and interfaces are tools used to define shared behaviors for multiple classes. While they have similarities, they serve slightly different purposes and are used in different scenarios.


    1. What is an Abstract Class?

    • An abstract class is like a blueprint. It can have both:
      • Abstract methods: These are methods without a body (no implementation) that subclasses must implement.
      • Concrete methods: These are methods with a body (already implemented) that can be inherited by subclasses.

    Key Points:

    • You cannot instantiate an abstract class (you can’t create objects from it directly).
    • An abstract class can have:
      • Properties (with or without values).
      • Methods (abstract or implemented).
      • Constructors.
    • Use abstract keyword for both the class and its methods.

    Example:

    abstract class Animal(val name: String) {
    abstract fun makeSound() // Abstract method
    fun eat() { // Concrete method
    println("$name is eating")
    }
    }

    class Dog(name: String) : Animal(name) {
    override fun makeSound() {
    println("$name says Woof!")
    }
    }

    fun main() {
    val dog = Dog("Buddy")
    dog.makeSound() // Output: Buddy says Woof!
    dog.eat() // Output: Buddy is eating
    }

    2. What is an Interface?

    • An interface is a collection of methods (and properties) that a class can implement. It defines what a class must do but not how it does it.
    • Starting from Kotlin 1.2, interfaces can also have default implementations for methods, but they cannot store state (i.e., no fields with values).

    Key Points:

    • A class can implement multiple interfaces (but can inherit only one abstract class).
    • Interfaces do not have constructors.

    Example:

    interface Flyable {
    fun fly() // Abstract method
    fun takeOff() { // Default implementation
    println("Taking off!")
    }
    }

    class Bird : Flyable {
    override fun fly() {
    println("The bird is flying")
    }
    }

    fun main() {
    val bird = Bird()
    bird.takeOff() // Output: Taking off!
    bird.fly() // Output: The bird is flying
    }

    Key Differences:

    FeatureAbstract ClassInterface
    PurposeTo share code and structure between related classes.To define a contract for unrelated classes.
    Multiple InheritanceA class can inherit only one abstract class.A class can implement multiple interfaces.
    StateCan have state (fields with values).Cannot have state (fields must be abstract).
    ConstructorsCan have constructors.Cannot have constructors.
    MethodsCan have both abstract and concrete methods.Can have abstract methods and default methods.
    PropertiesCan have concrete properties (with values).Properties are abstract by default (no backing field).

    When to Use Abstract Classes

    1. Use an abstract class when:
      • You want to share code and state among related classes.
      • You need a base class with a constructor.
      • Your classes are tightly related (e.g., Animal -> Dog, Cat).
    2. Abstract classes are suitable for hierarchies where you want a base class to enforce structure but also provide some functionality.

    When to Use Interfaces

    1. Use an interface when:
      • You want to define a contract that can be implemented by multiple unrelated classes.
      • You need to achieve multiple inheritance.
      • You want to share behavior without enforcing a class hierarchy.
    2. Interfaces are ideal for defining capabilities or roles (e.g., Flyable, Swimmable, Drivable).

    Combining Both

    Kotlin allows classes to inherit from an abstract class and implement multiple interfaces.

    Example:

    abstract class Animal(val name: String) {
    abstract fun makeSound()
    }

    interface Flyable {
    fun fly()
    }

    class Bird(name: String) : Animal(name), Flyable {
    override fun makeSound() {
    println("$name chirps")
    }

    override fun fly() {
    println("$name is flying")
    }
    }

    fun main() {
    val bird = Bird("Sparrow")
    bird.makeSound() // Output: Sparrow chirps
    bird.fly() // Output: Sparrow is flying
    }

    Quick Summary

    • Abstract Class: Use when you need a base class for related objects with shared code and state.
    • Interface: Use when you need to define a set of behaviors that can be implemented by unrelated classes or need multiple inheritance.

    Happy Koding!! 😊

  • Understanding dp and sp: A Complete Guide to Scalable UI Design in Android

    Understanding dp and sp: A Complete Guide to Scalable UI Design in Android

    In Android, dp (density-independent pixels) and sp (scale-independent pixels) are two units used to ensure that the UI design scales well across different screen sizes and densities. They help maintain a consistent and user-friendly appearance of elements on devices with varying screen resolutions and sizes.


    1. What is dp (Density-Independent Pixel)?

    • dp is a unit that helps create a consistent size for UI elements across devices with different screen densities.
    • 1 dp is equivalent to 1 pixel on a 160 dpi screen (baseline density).

    Why Use dp?

    • Using dp ensures that your UI elements (e.g., buttons, margins, padding) appear the same size on screens with different densities (low, medium, high, etc.).
    • The system automatically scales the dp value based on the screen density of the device.

    Conversion Formula:

    To convert dp to physical pixels:

    pixels = dp * (dpi / 160)

    For example:

    • On a 160 dpi screen, 1 dp = 1 px.
    • On a 320 dpi screen, 1 dp = 2 px.
    • On a 480 dpi screen, 1 dp = 3 px.

    Example:

    <Button
    android:layout_width="100dp"
    android:layout_height="50dp"
    android:text="Click Me" />
    • This button will have the same visual size on all devices, regardless of their screen density.

    2. What is sp (Scale-Independent Pixel)?

    • sp is similar to dp, but it also considers the user’s font size preferences (accessibility settings).
    • It is typically used for defining text sizes.

    Why Use sp?

    • Using sp ensures that text scales appropriately for users who have changed their font size in device settings for better readability.

    Example:

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textSize="16sp" />
    • The text size will adjust based on both the screen density and the user’s font size preferences.

    Key Differences Between dp and sp

    Aspectdp (Density-Independent Pixel)sp (Scale-Independent Pixel)
    Use CaseFor layout dimensions (e.g., margins, padding).For text sizes.
    ScalingScales based on screen density (dpi).Scales based on screen density and user’s font size settings.
    AccessibilityDoes not account for font size preferences.Accounts for font size preferences.

    When to Use dp vs sp

    1. Use dp:
      • For all layout dimensions (e.g., height, width, margins, padding).
      • For non-text elements like icons or shapes.
    2. Use sp:
      • Exclusively for text sizes, to ensure readability for users who adjust font sizes in their device settings.

    Practical Example

    XML Layout Using dp and sp:

    <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:orientation="vertical">

    <!-- Text with sp -->
    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello, World!"
    android:textSize="18sp" />

    <!-- Button with dp -->
    <Button
    android:layout_width="200dp"
    android:layout_height="50dp"
    android:text="Click Me" />
    </LinearLayout>

    Screen Density Reference

    Android categorizes devices by screen density:

    Density CategorydpiScale Factor (dp to px)
    ldpi (low)~1200.75
    mdpi (medium)~1601.0
    hdpi (high)~2401.5
    xhdpi (extra-high)~3202.0
    xxhdpi (extra-extra-high)~4803.0
    xxxhdpi (extra-extra-extra-high)~6404.0

    Key Takeaways

    1. dp ensures that UI components have consistent physical size across devices.
    2. sp ensures text remains readable, respecting user preferences for font size.
    3. Use dp for non-text elements and sp for text.

    😊

  • Classes are “final” by default in Kotlin

    In Kotlin, classes and methods are final by default, meaning they cannot be inherited or overridden. This design choice emphasizes safety and prevents accidental modification of classes or methods, which could lead to unexpected behaviors.

    To allow a class to be extended (inherited), you need to explicitly mark it with the open keyword. Similarly, methods and properties that you want to override in a subclass must also be marked with open.


    Why Use open for Classes?

    1. Explicit Inheritance Control:
      • Kotlin enforces explicit design choices. By default, making classes final prevents unintended extension, ensuring that your class behaves as expected without being altered.
      • By using open, you signal that the class is designed to be extended.
    2. Improved Code Safety:
      • It prevents accidental or unauthorized inheritance of classes, which could compromise encapsulation or lead to subtle bugs.
    3. Better Code Design:
      • You clearly indicate which classes are meant to be used as base classes, making your design intentions more readable.

    Example Without open

    class Parent {
    fun greet() {
    println("Hello from Parent")
    }
    }

    class Child : Parent() // Error: 'Parent' is final and cannot be inherited

    This results in a compilation error because Parent is final by default.


    Example With open

    open class Parent {
    open fun greet() {
    println("Hello from Parent")
    }
    }

    class Child : Parent() {
    override fun greet() {
    println("Hello from Child")
    }
    }

    fun main() {
    val child = Child()
    child.greet() // Output: Hello from Child
    }
    • Parent is marked open, allowing it to be extended by Child.
    • The greet method is also marked open, so it can be overridden.

    Why Not Use open by Default?

    1. Avoiding Unintended Changes:
      • If all classes were open by default, any developer could subclass or override a class or method, leading to unexpected behaviors in code.
    2. Encapsulation and Modularity:
      • Making classes and methods final by default helps maintain strong boundaries between components, encouraging better encapsulation.
    3. Easier Maintenance:
      • Final classes are easier to maintain because their behavior is fixed and predictable.

    When to Use open?

    • Use open only when you intend a class to serve as a base class.
    • Mark methods and properties open only if they are designed to be overridden in subclasses.
    • If a class or method should not be changed, leave it as final (default behavior).

    Final vs. Open vs. Abstract

    KeywordPurpose
    finalDefault behavior; classes and methods cannot be inherited or overridden.
    openAllows classes to be inherited and methods/properties to be overridden.
    abstractUsed for classes or methods that must be overridden in subclasses (cannot be instantiated directly).

    Example with Abstract and Open:

    abstract class Shape {
    abstract fun area(): Double
    open fun description() = "This is a shape"
    }

    class Circle(private val radius: Double) : Shape() {
    override fun area() = Math.PI * radius * radius
    override fun description() = "This is a circle"
    }
    • Shape is abstract, meaning it cannot be instantiated.
    • The area method is abstract, so it must be overridden.
    • The description method is open, so it can be overridden but is not mandatory.

    Summary

    • open explicitly indicates that a class or method is intended to be extended or overridden.
    • This approach ensures better code safety, readability, and maintainability compared to the default behavior in other languages where all classes are extendable by default.
  • Encapsulation using primary / secondary constructors

    Encapsulation in Kotlin involves restricting direct access to class variables and exposing them through controlled methods (getters and setters). In Kotlin, you can achieve encapsulation seamlessly using properties with custom accessors or by controlling the visibility of variables.

    Here’s how to add encapsulation to class variables when using primary or secondary constructors:


    1. Encapsulation with Primary Constructor

    The primary constructor can automatically define and initialize class properties with encapsulation using visibility modifiers (private, protected, etc.) and custom getters/setters.

    Example:

    class Person(private var _name: String, private var _age: Int) {
    // Public getter for name
    var name: String
    get() = _name
    set(value) {
    if (value.isNotBlank()) _name = value
    }

    // Public getter and setter for age with validation
    var age: Int
    get() = _age
    set(value) {
    if (value > 0) _age = value
    }

    init {
    println("Person created with Name: $_name, Age: $_age")
    }
    }

    // Usage
    val person = Person("Alice", 25)
    println(person.name) // Accessing name
    person.name = "Bob" // Changing name
    person.age = -5 // Invalid age; won't update
    println("Updated Name: ${person.name}, Age: ${person.age}")

    Explanation:

    • _name and _age are private and cannot be accessed directly outside the class.
    • name and age are public properties with controlled access.
    • Validation logic ensures only valid values are set.

    2. Encapsulation with Secondary Constructor

    For secondary constructors, you can initialize private variables and expose them through public properties.

    Example:

    class Car {
    private var _brand: String = ""
    private var _speed: Int = 0

    // Secondary constructor
    constructor(brand: String, speed: Int) {
    if (brand.isNotBlank()) _brand = brand
    if (speed > 0) _speed = speed
    }

    // Public getter and setter for brand
    var brand: String
    get() = _brand
    set(value) {
    if (value.isNotBlank()) _brand = value
    }

    // Public getter and setter for speed with validation
    var speed: Int
    get() = _speed
    set(value) {
    if (value > 0) _speed = value
    }
    }

    // Usage
    val car = Car("Toyota", 120)
    println("Brand: ${car.brand}, Speed: ${car.speed}")

    car.speed = 150 // Updates speed
    car.brand = "" // Invalid; won't update
    println("Updated Brand: ${car.brand}, Speed: ${car.speed}")

    Explanation:

    • _brand and _speed are private variables initialized in the secondary constructor.
    • brand and speed are public properties with controlled access.

    3. Encapsulation with private set

    Kotlin allows you to make the setter of a property private while keeping the getter public. This ensures that the property can only be modified within the class.

    Example:

    class BankAccount(val accountNumber: String, initialBalance: Double) {
    private var _balance: Double = initialBalance

    // Public read-only property
    val balance: Double
    get() = _balance

    // Public method to update balance
    fun deposit(amount: Double) {
    if (amount > 0) _balance += amount
    }
    }

    // Usage
    val account = BankAccount("12345", 1000.0)
    println("Account Balance: ${account.balance}")

    account.deposit(500.0) // Update balance through deposit method
    println("Updated Balance: ${account.balance}")

    Explanation:

    • balance is read-only outside the class but can be updated internally through the deposit() method.

    4. Encapsulation Using Visibility Modifiers

    Kotlin offers these visibility modifiers:

    • private: Accessible only within the class.
    • protected: Accessible within the class and its subclasses.
    • internal: Accessible within the same module.
    • public: Accessible from anywhere.

    Example:

    class Employee(private val id: Int, private var salary: Double) {
    fun showDetails() {
    println("Employee ID: $id, Salary: $salary")
    }

    fun updateSalary(newSalary: Double) {
    if (newSalary > 0) {
    salary = newSalary
    }
    }
    }

    // Usage
    val emp = Employee(101, 50000.0)
    emp.showDetails()
    // emp.id or emp.salary cannot be accessed directly

    Best Practices

    1. Use private variables to restrict direct access to sensitive data.
    2. Expose data using custom getters and setters to validate or control access.
    3. Use read-only properties (val) for values that shouldn’t change after initialization.
    4. Combine encapsulation with Kotlin’s visibility modifiers for better control.

    😊

  • Primary and Secondary constructors in Kotlin

    In Kotlin, constructors are used to initialize objects when a class is instantiated. Kotlin provides two types of constructors:

    1. Primary Constructor
    2. Secondary Constructor

    1. Primary Constructor

    • The primary constructor is part of the class header and is used to initialize properties of the class.
    • It is concise and typically used when a class has straightforward initialization needs.

    Syntax:

    class ClassName(param1: Type, param2: Type) {
    // Initialization block (if needed)
    init {
    // Code to initialize or process properties
    }
    }

    Example:

    class Person(val name: String, var age: Int) {
    // `init` block to add additional initialization logic
    init {
    println("Name: $name, Age: $age")
    }
    }

    // Create an instance
    val person = Person("Alice", 25)

    Explanation:

    • val name: String and var age: Int in the primary constructor automatically create and initialize properties.
    • The init block executes when the object is created.

    2. Secondary Constructor

    • A secondary constructor is defined inside the class body using the constructor keyword.
    • It provides alternative ways to instantiate the class when additional initialization logic is required or the primary constructor doesn’t fit.

    Syntax:

    class ClassName {
    // Secondary constructor
    constructor(param1: Type, param2: Type) {
    // Code for initialization
    }
    }

    Example:

    class Person {
    var name: String
    var age: Int

    // Secondary constructor
    constructor(name: String, age: Int) {
    this.name = name
    this.age = age
    println("Name: $name, Age: $age")
    }
    }

    // Create an instance
    val person = Person("Bob", 30)

    Explanation:

    • The secondary constructor explicitly initializes the properties name and age.
    • It can contain custom initialization logic specific to the secondary constructor.

    Combining Primary and Secondary Constructors

    When both primary and secondary constructors are present:

    • Secondary constructors must delegate to the primary constructor (directly or indirectly) using the this keyword.

    Example:

    class Person(val name: String, var age: Int) {
    var city: String = "Unknown"

    // Secondary constructor delegating to the primary constructor
    constructor(name: String, age: Int, city: String) : this(name, age) {
    this.city = city
    }

    init {
    println("Primary constructor: Name: $name, Age: $age")
    }
    }

    // Create instances
    val person1 = Person("Charlie", 40) // Uses primary constructor
    val person2 = Person("Dave", 35, "New York") // Uses secondary constructor

    println(person2.city) // Output: New York

    Explanation:

    • The primary constructor initializes name and age.
    • The secondary constructor adds initialization for city and ensures delegation to the primary constructor using : this(name, age).

    Key Differences Between Primary and Secondary Constructors

    FeaturePrimary ConstructorSecondary Constructor
    Definition LocationIn the class header.Inside the class body.
    PurposeSimplifies initialization of properties.Provides alternative ways to create objects.
    DelegationCannot delegate to secondary constructors.Must delegate to the primary constructor.
    Use CasesSimple and common initialization.Complex initialization or alternative setups.

    When to Use Which?

    • Use primary constructors for straightforward property initialization (preferred for most cases).
    • Use secondary constructors when:
      • You need multiple ways to initialize the class.
      • Complex initialization logic is required.

    😊