A quick introduction to Kotlin programming

Object-oriented programming

Unlike procedural languages such as C, object-oriented languages allow you to create classes. A class is a kind of encapsulation of things.

In a nutshell, it is to encapsulate things into concrete classes, and then define the properties and capabilities of things into fields and functions in the class, respectively. Then the class is instantiated, and the fields and methods in the class are called according to the specific programming requirements.

Classes and objects

class Person {
    var name = ""
    var age = 0
    
    fun eat() {
        println(name + " is eating. He is " + age + " years old.")
    }
}

val p = Person()
  • Kotlin also uses the class keyword to declare a class
  • The var keyword creates the name and age fields because we need to specify the name and age after the object is created, whereas the val keyword cannot be reassigned after initialization
  • Kotlin instantiates a class in much the same way as Java, except without the new keyword

Inheritance and constructors

You can have the Student class inherit from the Person class, so that Student automatically owns the fields and functions in Person, plus you can define your own fields and functions. In order for the Student class to inherit from the Person class, we have to do two things

  • Precede the Person class with the open keyword to make it inheritable
  • The inherited keyword in Java is extends, but in Kotlin it becomes a colon

    class Student : Person() {
      var sno = ""
      var grade = 0
    }

The tricky thing about inheritance is why do we have parentheses after the Person class? You don’t need parentheses for inheritance in Java. To see why, let’s first look at some constructors in Kotlin.

Any object-oriented programming language has the concept of constructors, and Kotlin does, too, but Kotlin divides constructors into two types: primary and secondary.

The primary constructor will be your most common constructor. By default, every class will have a primary constructor that takes no arguments, although you can explicitly specify arguments to it. The main constructor has no function body and is defined directly after the class name. Like this:

class Student(val sno: String, val grade: Int) : Person() {

}

Okay, so far, that makes sense, right? But what does that have to do with the parentheses? This refers to a rule in the Java inheritance feature that a constructor in a subclass must call a constructor in a superclass, a rule that Kotlin also adheres to.

So take a look back at Student. Now we have declared a primary constructor. By inheritance, the subclass constructor must call the parent constructor. You might say, well, you can just call it in the init structure. That might be one way to do it, but it’s definitely not a good way to do it, because in most cases, you don’t need to write an init structure.

Kotlin, of course, doesn’t use this design, but instead uses another simple but perhaps less understandable design approach: parentheses. Which constructor of the parent class is called by the primary constructor of a child class is specified by parentheses during inheritance. So look at the code again, and you should get the idea.

class Student(val sno: String, val grade: Int) : Person() {
    
}

In this case, the pair of empty parentheses after the Person class indicates that the main constructor of the Student class calls the no-argument constructor of the Person class when it is initialized. The parentheses cannot be omitted even if there are no arguments.

If we changed Person to put the name and age in the main constructor, it would look something like this:

open class Person(val name: String, val age: Int) {
    ...
}

Your Student class is going to report an error, and the reason for the error is obvious. The empty parentheses after the Person class indicate that we’re going to call the Person class’s parameterless constructor, but the Person class doesn’t have any parameterless constructors anymore, so we’re going to get the error.

If we want to resolve this error, we have to pass the name and age fields to the Person constructor, which are not present in the Student class either. It’s easy. If you don’t have it, add it. We can add the name and age arguments to the Student class’s main constructor and pass them to the Person class’s constructor as follows:

class Student(val sno: String, val grade: Int, name: String, age: Int) Person(name, age) {
    ...
}

So now that we’ve got Kotlin’s main constructor, don’t you think the parenthesis problem in inheritance is not that hard to understand? The complexity of Kotlin’s parentheses doesn’t stop there, however, because we haven’t touched on another component of Kotlin’s constructors, the sub-constructors.

In fact, you rarely need sub-constructors. Kotlin provides a way to set default values for the parameters of functions, which is basically a substitute for sub-constructors, which we’ll learn about at the end of this chapter. But for the sake of structural integrity, I decided to cover the next constructor and explore the difference between parentheses and subconstructors.

You should know that any class can have only one primary constructor, but it can have multiple secondary constructors. A secondary constructor can also be used to instantiate a class, not unlike a primary constructor, except that it has a function body.

Kotlin states that when a class has both a primary constructor and a secondary constructor, all the secondary constructors must call the primary constructor (including indirect calls). Here IS a concrete example to illustrate the simple, as follows:

class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) {

    constructor(name: String, age: Int) : this("001", 1, name, age)

    constructor() : this("geely", 24)

}

Sub-constructors are defined using the constructor keyword. Here we define two sub-constructors: the first sub-constructor takes name and age, which then calls the main constructor using this keyword and assigns the sno and grade arguments to initial values. The second sub-constructor takes no arguments. It calls the first sub-constructor we just defined with the this keyword and assigns the name and age arguments to their initial values as well. Since the second sub-constructor indirectly calls the main constructor, this is still legal.

So now we have three ways to materialize the Student class: with a constructor that takes no arguments, with a constructor that takes two arguments, and with a constructor that takes four arguments. The corresponding code looks like this:

val student1 = Student()
val student2 = Student("Jack", 19)
val student3 = Student("a123", 5, "Jack", 19)

So we’ve covered the use of the sub-constructor, but so far, the parenthesis problem with inheritance doesn’t go any further. For the time being, it’s the same as in the previous scenarios.

So let’s look at a very special case: a class that has only a secondary constructor and no primary constructor. It’s really rare, but it’s allowed in Kotlin. When a class does not explicitly define a primary constructor and defines sub-constructors, it has no primary constructor. Let’s take a look at the code:

class Student : Person {
    constructor(name: String, age: Int) : super(name, age) {
    }
}

Notice the code changes here. First of all, the primary constructor is not explicitly defined after the Student class. At the same time, since the secondary constructor is defined, the Student class now has no primary constructor. Since there is no primary constructor, there is no need to extend the Person class with parentheses. Reason is so simple, just a lot of people start learning to Kotlin failed to understand the meaning of the brackets and rules, so always feel the inheritance method sometimes make add parentheses, sometimes don’t add, get dizzy, but after you understand the rules, you will find it is easy to understand.

In addition, since there is no primary constructor, the secondary constructor can only call the parent constructor directly. This code also replaces the this keyword with the super keyword. This part is easy to understand, and I won’t go into more detail because it is similar to Java.

This section explores Kotlin’s inheritance and constructors in depth, and it’s one of the most difficult parts to understand when you’re new to Kotlin.

interface

In Java, the inherited keyword is extends, and the implementation interface keyword is implements, while Kotlin uses a uniform colon, separated by commas. In addition, there is no parentheses after the interface, because it has no constructor to call. Let’s look at the code:

class Student(name: String, age: Int) : Person(name, age), Study {
    override fun readBooks() {
        println(name + " is reading.")
    }
    override fun doHomework() {
        println(name + " is doing homework.")
    }
}

Data class and singleton class

data class Cellphone(val brand: String, val price: Double)

Creating a data class with Kotlin is as simple as one line of code, you read that right, one line of code! The magic is the data keyword. When you declare the data keyword in front of a class, you want that class to be a data class, Kotlin reduces your development effort by automatically generating equals(), hashCode(), toString(), and so on, based on the arguments in the main constructor.

Creating a singleton class in Kotlin is as simple as changing the class keyword to the object keyword.

object Singleton {
    fun singletonTest() {
        println("singletonTest is called.")
    }
}

As you can see, in Kotlin we don’t need to privatize the constructor, or provide a static method like getInstance(), just change the class keyword to the object keyword, and a singleton class is created. Calling a function in a singleton class is simple, similar to how static methods are called in Java:

Singleton.singletonTest()

This looks like a call to a static method, but Kotlin automatically creates an instance of the Singleton class behind it and guarantees that only one Singleton instance will exist globally.

Lambda programming

Kotlin has supported Lambda programming since the first version, and lambdas in Kotlin are so powerful that I even think Lambda is the soul of Kotlin.

However, this chapter is only an introduction to Kotlin, and I cannot cover all aspects of Lambda in this short section. Therefore, in this section we will cover only the basics of Lambda programming, while the more advanced Lambda techniques, such as higher-order functions and DSLS, will be picked up later in the book.

Collection creation and traversal

The functional API for collections is a great example of how to get started with Lambda programming, but to do that, we need to learn how to create collections first.

Kotlin specifically provides a built-in listOf() function to simplify the initialization of the collection, as shown below:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")

A for-in loop can be used to traverse not only intervals but also collections. Now let’s try using a for-in loop to iterate over this set of fruits. Write the following code inside the main() function:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
    for (fruit in list) {
        println(fruit)
    }
}

Map is a data structure in the form of key-value pairs, so it is quite different from List and Set in usage.

Instead of using the put() and get() methods to add and read data from a Map, Kotlin recommends using a syntactic structure similar to array subscripts. For example, to add a piece of data to a Map, write:

map["Apple"] = 1

To read a piece of data from a Map, we can write:

val number = map["Apple"]

Of course, this is still not the easiest way to write it, because Kotlin undoubtedly provides a pair of mapOf() and mutableMapOf() functions to continue simplifying the use of maps. In the mapOf() function, we can create a Map set by passing in the initialized key-value pair:

val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)

A functional API for collections

There are many functional apis, and I’m not going to walk you through all of them. I’m going to focus on the syntax of functional apis, namely the syntax of Lambda expressions.

First let’s think about a requirement. How do I find the longest word in a collection of fruits? Of course this requirement is simple and can be written in many different ways. You might naturally write code like this:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") var maxLengthFruit = "" for (fruit in list) { if (fruit.length > maxLengthFruit.length) { maxLengthFruit =  fruit } } println("max length fruit is $maxLengthFruit")

This code is very concise, the idea is very clear, can be said to be a pretty good code. But we can make this easier if we use the collection’s functional API:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val maxLengthFruit = list.maxBy{it.length}
prinln("max length fruit is $maxLengthFruit")

The above code uses the functional API to find the fruit with the longest word in the set in a single line of code. If you’re having trouble understanding this code right now, it’s because we haven’t started learning the syntax of Lambda expressions yet, and when you revisit the code after you’ve learned it, it’s pretty straightforward.

First, take a look at the definition of a Lambda, which, in plain language, is a small piece of code that can be passed as a parameter. By definition, this is a great feature, because normally we can only pass variables to a function, but with Lambda we can pass a small piece of code. The phrase “a small piece of code” is used twice here, so how much code is a small piece of code? Kotlin does not place any restrictions on this, but it is generally not recommended to write too long code in Lambda expressions, as it may affect the readability of the code.

Next, let’s look at the syntax of Lambda expressions:

{parameter 1: parameter type, parameter 2: parameter type -> function body}

This is the most complete syntactic structure definition of a Lambda expression. First of all, the outermost layer is a pair of curly braces, if there is a parameter passed to Lambda expressions, we also need to declare the parameter list, at the end of the argument list to use a – > symbol, said the end of the parameter list and the beginning of the function body, the body of the function can be written in any line of code (though long code) is not recommended, And the last line of code is automatically returned as a Lambda expression.

Of course, in many cases, we don’t need to use the full syntactic structure of a Lambda expression, but there are many simplified ways to write it. So let’s start with the simple stuff.

Going back to the need to find the longest word fruit, the syntax of the functional API used earlier may seem special, but maxBy is just a normal function that takes a Lambda argument. The value of each iteration is passed as a parameter to the Lambda expression as the collection is traversed. The maxBy function works by iterating through the set to find the maximum value of the given condition. For example, if you want to find the fruit with the longest word, the condition should be the length of the word.

Now that we understand how maxBy works, we can begin to apply the syntax we learned for Lambda expressions and pass it into maxBy as follows:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lambda = {fruit: String -> fruit.length}
val maxLengthFruit = list.maxBy(lambda)

As you can see, the maxBy function essentially takes a Lambda argument, and the Lambda argument is defined exactly as the syntax of the expression you just learned, so this code should be fairly easy to understand.

This method works well, but it is verbose and can be simplified a lot, so let’s start simplifying this code step by step.

  1. First, we don’t need to define a lambda variable. Instead, we can pass the lambda expression directly into the maxBy function, so the first step is simplified as follows:

    val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })
  2. Kotlin then specifies that when the Lambda argument is the last argument to the function, the Lambda expression can be moved outside the function parentheses, as follows:

    val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }
  3. Next, you can omit the parentheses if the Lambda argument is the only argument to the function:

    val maxLengthFruit = list.maxBy{ fruit: String -> fruit.length }
  4. Does that make the code look a lot cleaner? But we can still simplify. Because Kotlin has an excellent type derivation mechanism, the argument lists in Lambda expressions don’t actually have to declare the argument types in most cases, so the code can be further simplified as follows:

    val maxLengthFruit = list.maxBy{ fruit -> fruit.length }
  5. Finally, when the argument list of a Lambda expression has only one argument, it is not necessary to declare the argument name. Instead, you can use the IT keyword, and the code becomes:

    val maxLengthFruit = list.maxBy { it.length }

    How’s that? And by doing this step by step, we’ve got exactly the same functional API that we started with, so it’s pretty easy to understand now, isn’t it?

Next we will learn a few sets of more commonly used functional API, believe these for you now, should be no difficulty.

The map function in a collection is one of the most commonly used functional apis. It is used to map each element in a collection to an additional value. The rules for mapping are specified in a Lambda expression, resulting in a new collection. For example, if we want all the fruit names to be capitalized, we could write:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

The map function is so powerful that it can transform the elements of a collection in any way we want, but this is just a simple example. In addition, you can convert all fruit names to lowercase, or just the first letter of the word, or even to a set of numbers such as the length of the word, simply by writing the logic you need in the Lambda representation.

Let’s take a look at another common functional API — the filter function. As the name implies, the filter function is used to filter the data in the set. It can be used alone or in conjunction with the map function.

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.filter { it.length <= 5 }
        .map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

Let’s move on to two of the more common functional apis — the any and all functions. The any function is used to determine whether at least one element in the set satisfies the specified condition, and the all function is used to determine whether all elements in the set satisfy the specified condition. Since both functions are easy to understand, let’s go straight to the code example:

fun main() { val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") val anyResult = list.any { it.length <= 5 } val allResult = list.all { it.length <= 5 } println("anyResult  is " + anyResult + ", allResult is " + allResult) }

There are many other functional apis in the collection, but once you have the basic grammar rules, the rest of the functional apis can be mastered by just looking at the documentation. I’m sure it’s not too hard for you to learn.

Null pointer check

The exception type with the highest crash rate on Android is NullPointerException. I believe that not only Android, other systems are facing the same problem. The root cause is that null Pointers are run-time exceptions that are not checked by the programming language and can only be avoided by the programmer’s active logical judgment, but even the best programmer can’t take all potential null Pointers into account.

Let’s look at some very simple Java code:

public void doStudy(Study study) {
    study.readBooks();
    study.doHomework();
}

This is one of the doStudy() methods we wrote earlier, and I translated it into the Java version. This code doesn’t have any complicated logic. It just accepts a Study parameter and calls its readBooks() and doHomework() methods.

Is this code secure? Not necessarily, because it depends on what arguments are passed by the caller. If we pass a null argument to the doStudy() method, there is no doubt that a null pointer exception will occur here. Therefore, it is more prudent to make a null judgment before calling the parameter’s method, as shown below:

public void doStudy(Study study) {
    if (study != null) {
        study.readBooks();
        study.doHomework();
    }
}

This ensures that the code is always safe, no matter what parameters are passed in.

As you can see, even such a simple piece of code has the potential to generate a null pointer exception, and it is almost impossible to completely avoid null pointer exceptions in a large project, which is why it tops the list of crashes.

Nullable type system

Kotlin, however, tackles the problem scientifically by almost eliminating null pointer exceptions with a compile-time null-check mechanism. While the compile-time null-checking mechanism can sometimes make code difficult to write, don’t worry, Kotlin provides a number of auxiliary tools that make it easy to handle all kinds of null-checking. Let’s start learning step by step.

Back to the doStudy() function, now translate the function back to the Kotlin version as follows:

fun doStudy(study: Study) {
    study.readBooks()
    study.doHomework()
}

This code doesn’t look any different from the Java version, but there is no null pointer risk, because Kotlin defaults to non-null parameters and variables, so the Study parameter passed here is definitely not null, and we can safely call any of its functions. If you try to pass a null argument to the doStudy() function, you get an error like the one shown in the figure below.

Now, you might be wondering, all parameters and variables can’t be null, right? This is really unheard of, but what if our business logic simply requires a parameter or variable to be empty? Don’t worry, Kotlin provides an alternative nullable type system, but with a nullable type system, we need to get rid of any potential null-pointer exceptions at compile time or the code won’t compile.

So what does a nullable type system look like? Simply put a question mark after the class name. For example, Int represents an integer that is not null, whereas Int? Is an integer that can be null; String means a String that cannot be empty, whereas String? Represents a nullable string.

Going back to doStudy(), if we want to pass an empty argument, we should change the type of the argument from Study to Study? , as shown in the figure below.

As you can see, the doStudy() function is now called with null arguments and no longer prompts for errors. However, when you call the readBooks() and doHomework() methods in the doStudy() function, you get an error message with a red sliding line. Why?

And the reason is obvious, because we changed the parameter to a nullable Study, right? Type, where both the readBooks() and doHomework() methods of the argument can cause null pointer exceptions, so Kotlin will not allow the compilation to pass in this case.

So what’s the solution? As simple as handling null pointer exceptions, for example, do something like this:

fun doStudy(study: Study?) { if (study ! = null) { study.readBooks() study.doHomework() } }

The code should now compile and pass without null pointer exceptions at all.

Kotlin’s nullable type system and null-pointer checking are pretty well understood here, but it usually takes a lot of extra checking to get rid of all null-pointer exceptions at compile time. If you use an if statement at every point in your code, it makes your code more verbose, and the if statement doesn’t deal with nullability for global variables. To this end, Kotlin provides a series of ancillary tools to make it easier for developers to do this, so let’s take a look at each of them.

Air detection AIDS

First study the most commonly used? . Operator. It’s easy to understand what this operator does. It calls the method normally when the object is not empty, and does nothing when the object is empty. For example, the following null handler code:

if (a ! = null) { a.doSomething() }

This code uses? The. Operator can be simplified as:

a? .doSomething()

Understand? The.operator is used to optimize the doStudy() function. Let’s look at how this operator can be used to optimize the doStudy() function.

fun doStudy(study: Study?) { study? .readBooks() study? .doHomework() }

Now let’s look at another very common one, right? The: operator. This operator receives an expression on both the left and right sides and returns the result of the left expression if it is not null, or the result of the right expression otherwise. Look at the following code:

val c = if (a ! = null) {
    a
} else {
    b
}

The logical use of this code? The: operator can be simplified as:

val c = a ? : b

Now let’s combine this with a specific example, right? . And? : to give you a better understanding of them.

For example, if we want to write a function to get the length of a piece of text, we can write it in the traditional way:

fun getTextLength(text: String?) : Int { if (text ! = null) { return text.length } return 0 }

Since text can be empty, we need to do a null check first, returning the length of the text if it is not empty, or 0 if it is.

This code doesn’t look complicated, but we can make it much easier with operators like this:

fun getTextLength(text: String?) = text? .length ? : 0

Here we are going to? . And? The: operator is used together. First, since text can be empty, we need to use it when calling its length field. . Operator, and when text is empty, text? .length returns a null value, at which point we use the? The: operator makes it return 0. So, do you think these operators are getting better and better?

Kotlin’s null-pointer checking isn’t always smart, though. There are times when a null pointer exception might have been handled logically, but Kotlin’s compiler doesn’t know it, and it will still fail.

Look at the following code example:

var content: String? = "hello" fun main() { if (content ! = null) { printUpperCase() } } fun printUpperCase() { val upperCase = content.toUpperCase() println(upperCase) }

Here we define a null global variable, content, and then we do a null check in main(), and then we call printUpperCase() when content is not null. In printUpperCase(), We convert the content to uppercase and print it out.

It looks like there’s nothing wrong with the logic, but unfortunately, this code must not work. Because the printUpperCase() function doesn’t know that the content variable has been checked externally for non-nullability, when it calls the toUpperCase() method, it also thinks that there is a null pointer risk and therefore won’t compile through.

In this case, if we want to force through compilation, we can use the non-empty assertion tool, which is written by adding!! To the end of the object. , as shown below:

fun printUpperCase() { val upperCase = content!! .toUpperCase() println(upperCase) }

This is a risky way of saying to Kotlin, I’m pretty sure this object is not null, so I don’t need you to do a null pointer check for me, and if something goes wrong, you can just throw a null pointer exception at my own expense.

Finally, we will learn a different AIDS – let. Let is neither an operator nor a keyword, but a function. This function provides the programming interface to the functional API and passes the original calling object as an argument to the Lambda expression. The sample code is as follows:

Obj. let {obj2 -> // write the concrete business logic}

As you can see, the let function of the obj object is called, and the code in the Lambda expression is immediately executed, and the obj object itself is passed as an argument to the Lambda expression. However, I changed the parameter name to obj2 to prevent the same variable name, but in fact they are the same object, which is what the let function does.

Let functions are standard functions in Kotlin, and we’ll learn more about Kotlin’s standard functions in the next chapter.

You might be asking, what does this let function have to do with null pointer checking? What about the let function? The. Operator plays a big role in null pointer checking.

Returning to the doStudy() function, the current code looks like this:

fun doStudy(study: Study?) { study? .readBooks() study? .doHomework() }

Although this code we passed? The. Operator is optimized to compile properly, but this is a bit verbose. If you translate this code into the if statement, it would look like this:

fun doStudy(study: Study?) { if (study ! = null) { study.readBooks() } if (study ! = null) { study.doHomework() } }

In other words, we can call any method on the study object with an if judgment, but we are limited to? The. Operator is now restricted to an if judgment every time a method on the study object is called.

You can use it together, right? . Operator and let function to optimize the code as follows:

fun doStudy(study: Study?) { study? .let { stu -> stu.readBooks() stu.doHomework() } }

Let me explain the above code briefly,? The let function passes the study object itself as an argument to the Lambda expression. At this point, the study object must not be empty, and we can safely call any of its methods.

Also remember the syntactic nature of Lambda expressions? When there is only one argument in the argument list of a Lambda expression, you can simply use the IT keyword instead of declaring the argument name. Then the code can be further simplified to:

fun doStudy(study: Study?) { study? .let { it.readBooks() it.doHomework() } }

Before I conclude this section, I should add that the let function handles nullability for global variables, whereas the if statement does not. For example, if we change the argument in the doStudy() function to a global variable, the let function will still work, but the if statement will prompt an error, as shown in the figure below.

The reason for the error is that the value of the global variable can be changed by another thread at any time. There is no guarantee that the study variable in the if statement does not have a null pointer risk. From this point can also reflect the advantages of the let function.

Ok, so that’s about the most common Kotlin null-pointer checking AIDS, and once you get the hang of this section, you’ll be able to write more robust code that almost eliminates null-pointer exceptions.

Kotlin’s magic trick

String template

First, look at the syntax rules for nested string expressions in Kotlin:

"hello, ${obj.name}. nice to meet you!"

As you can see, Kotlin allows us to embed a ${} syntactically structured expression in a string and replace that with the result of the expression’s execution at run time.

Default values for the arguments to the function

In the above code, there is a primary constructor and two secondary constructors. The secondary constructor here provides a way to instantiate the Student class with fewer arguments. A subconstructor that takes no arguments calls the subconstructor that takes two arguments and assigns them to the initial values. The two-argument secondary constructor calls the four-argument primary constructor and assigns the missing two arguments to their initial values. This is unnecessary in Kotlin, because we can do it by writing only one main constructor and setting the default values for the arguments, as follows:

class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age) {
}