Is there a type of code that you can read from beginning to end with clear semantics? It’s like reading an English article. This article will take a look at the simplicity of the Kotlin language features and the underlying principles behind them to implement business requirements in such literal code.
It includes sequences, set operations, main constructors, variable parameters, default parameters, named parameters, for loops, data classes. In the spirit of pragmatism, instead of going into all the details of the knowledge (which would be boring), only the relevant aspects of the knowledge are mentioned.
The business requirements are as follows: Suppose that you need to filter out the elective courses of all students based on the student list (the number of class hours is less than 70). The output is arranged in ascending order by the number of class hours. If the number of class hours is equal, the output is arranged in alphabetical order by the course name, and the first letter of the course name should be capitulated.
Data classes
First, declare the data entity class. Java code looks like this:
Course Entity
public class Course {
private String name ;
private int period ;
private boolean isMust;
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPeriod(a) {
return period;
}
public void setPeriod(int period) {
this.period = period;
}
public boolean isMust(a) {
return isMust;
}
public void setMust(boolean must) {
isMust = must;
}
@Override
public String toString(a) {
return "Course{" +
"Name =" " + name + '\' ' +
", period=" + period +
", isMust=" + isMust +
'} '; }}Copy the code
Student Entity class
public class Student {
private String name;
private int age;
private boolean isMale ;
private List<Course> courses ;
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(a) {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isMale(a) {
return isMale;
}
public void setMale(boolean male) {
isMale = male;
}
public List<Course> getCourses(a) {
return courses;
}
public void setCourses(List<Course> courses) {
this.courses = courses;
}
@Override
public String toString(a) {
return "Student{" +
"name='" + name + '\' ' +
", age=" + age +
", isMale=" + isMale +
", courses=" + courses +
'} '; }}Copy the code
The code is a little longer, but there are only two key pieces of information: class name + attribute, and the rest is template code, so Kotlin reduced the definition of the data class to a single line of code:
data class Course constructor(var name: String, var period: Int.var isMust: Boolean = false)
data class Student constructor(var name: String, var age: Int.var isMale: Boolean.var courses: List<Course> = listOf())
Copy the code
data
Is a reserved word used to decorate a class to indicate that it contains only data and not behavior, i.e., the Bean class in Java.- Class is declared in the following format:
modifierclassThe name of the classconstructor(main constructor argument list)Copy the code
class
Reserved words are used to declare classes.constructor
Reserved word used to declare a classMain construction methodThis is equivalent to combining the class declaration and constructor declaration in Java into one line. The following two pieces of code are completely equivalent:
//java
class A(a){
private int i
A(int i){
this.i = i; }}Copy the code
//kotlin
class A constructor(var i: Int)
Copy the code
- Main construction methodThe member attributes of the class and their data types are explicitly declared. The hidden information contained here is when constructing
A
The Int value passed to the constructor is assigned to the member I. - The main constructor contains no logic other than simply assigning values to members. (Other methods are needed when special initialization logic is required, more on that later)
- When there is no visibility modifier to modifyMain construction methodCan be omitted
constructor
Reserved words, so the data class above can be simplified to:
data class Course(var name: String, var period: Int, var isMust: Boolean = false)
data class Student(var name: String, var age: Int, var isMale: Boolean, var courses: List<Course> = listOf())
Copy the code
It also shows a feature that is not supported in Java: The default value of the argument, the default value of the isMust property of the Course class is false, which reduces the number of overloaded constructors that can only be implemented by overloading in Java:
public Course{
public Course(String name,int period,boolean isMust){
this.name = name;
this.period = period;
this.isMust = isMust;
}
public Course(String name,int period){
return Course(name,period,false); }}Copy the code
Behind the simple one-sentence class declaration, the compiler automatically creates all the methods we need, including:
- Setter () and getter ()
- Equals () and hashCode ()
- toString()
- copy()
Where copy() builds a new object based on the object’s existing property values.
Build collections
Now that you have the data entity class, you can build the data set. Let’s build a list of 4 students in Java code like this (it’s a good idea to skip this code, because it’s verbose and unreadable) :
Student student1 = new Student();
student1.setName("taylor");
student1.setAge(33);
student1.setMale(false);
List<Course> courses1 = new ArrayList<>();
Course course1 = new Course();
course1.setName("pysics");
course1.setPeriod(50);
course1.setMust(false);
Course course2 = new Course();
course2.setName("chemistry");
course2.setPeriod(78);
courses1.add(course1);
courses1.add(course2) ;
student1.setCourses(courses1);
Student student2 = new Student();
student2.setName("milo");
student2.setAge(20);
student2.setMale(false);
List<Course> courses2 = new ArrayList<>();
Course course3 = new Course();
course3.setName("computer");
course3.setPeriod(50);
course3.setMust(true);
student2.setCourses(courses2);
List<Student> students = newArrayList<>(); students.add(student2); students.add(student1); .Copy the code
I only wrote 2 student builds and don’t want to write any more… Can you see at a glance what it’s building?
Here’s how Kotlin plays it:
val students = listOf(
Student("taylor".33.false, listOf(Course("physics".50), Course("chemistry".78))),
Student("milo".20.false, listOf(Course("computer".50.true))),
Student("lili".40.true, listOf(Course("chemistry".78), Course("science".50))),
Student("meto".10.false, listOf(Course("mathematics".48), Course("computer".50.true))))Copy the code
Even when you first meet Kotlin, you know what this is all about.
- Thanks to theParameter Default values, for the same
Course
Constructor, which can pass two argumentsCourse("physics", 50)
, or three parameters can be passedCourse("computer", 50, true)
listOf()
Is a method in the Kotlin standard library. This method greatly simplifies the code for building collections. Take a look at its source:
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
Copy the code
vararg
Reserved words are used to modifyVariable parameterThis function can accept any number of arguments of this class.listOf()
The return value of is in kotlinList
Type.
At a glance, we can see that this code builds a list of four student instances, building a series of course instances along with the student instances.
But what is the semantics of the Boolean value passed in when building students? Guess may be the age, with the help of the IDE jump function, you can easily go to the Student definition to confirm. This is not the case if you are doing Code Review on the web.
Is there any way to specify the semantics of parameters at the point of a method call?
This is where the named parameter function comes in. The code above can also be written like this:
val students = listOf(
Student("taylor".33, isMale = false, courses = listOf(Course("physics".50), Course("chemistry".78))),
Student("milo".20, isMale = false, courses = listOf(Course("computer".50.true))),
Student("lili".40, isMale = true, courses = listOf(Course("chemistry".78), Course("science".50))),
Student("meto".10, isMale = false, courses = listOf(Course("mathematics".48), Course("computer".50.true))))Copy the code
The semantics of the specified parameter can be displayed by adding the variable name = before the parameter, which also puts forward higher requirements for the naming of variables.
As programmers, we spend most of our time not writing but reading other people’s code or our own. Just like the Chinese teacher to read a large number of essays, if the handwriting is sloppy, paragraph is not clear, is to give yourself to the teacher trouble. Similarly, the meaning of the name, consistent indentation, clear semantic call, let oneself and colleagues pleasing to the eye. (This is why Kotlin is so much more efficient, because it’s more concise and readable.)
Manipulate collections
The next step is to manipulate the set and go directly to Kotlin:
val friends = students
.flatMap { it.courses }
.toSet()
.filter { it.period < 70 && !it.isMust }
.map {
it.apply {
name = name.replace(name.first(), name.first().toUpperCase())
}
}
.sortedWith(compareBy({ it.period }, { it.name }))
Copy the code
As a sweep, one of the most unfamiliar functions we’ve seen in the last article is apply(). All it does is capitalize the first character of the name attribute on every element in the collection.
In Java (prior to 8.0), it was necessary to use a for loop to traverse the collection in order to manipulate the collection elements. But in the above code, no similar traversal operation is found, so how does Kotlin get the elements in the collection?
map()
The kotlin standard library prefixes a number of collection manipulation methods. Map () is one of them. The source code is as follows:
public inline fun <T, R>可迭代<T>.map(transform: (T) - >R): List<R> {
return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
Copy the code
Map () is an extension of the Iterable class, which represents an object that can be iterated over, from which both Collection and List inherit:
/**
* Classes that inherit from this interface can be represented as a sequence of elements that can
* be iterated over.
* @param T the type of element being iterated over. The iterator is covariant on its element type.
*/
public interface 可迭代<out T> {
/** * Returns an iterator over the elements of this object. */
public operator fun iterator(a): Iterator<T>
}
public interface Collection<out E> : 可迭代<E> {... }public interface List<out E> : Collection<E> {... }Copy the code
Map () creates a new collection of type ArrayList (which is an intermediate temporary collection) and passes it to mapTo().
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) - >R): C {
for (item in this)
destination.add(transform(item))
return destination
}
Copy the code
Here comes the familiar reserved word for, which when collocated in resembles Java’s for-each semantics.
The original map() internally uses a for loop to traverse the source collection, apply the transform to each element, and finally add the transformed element to the temporary collection and return it.
So the semantics of the map() function are: apply a custom transformation to each element of the collection
filter()
Filter () is called before the map() function. The source code is as follows:
public inline fun <T>可迭代<T>.filter(predicate: (T) - >Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) - >Boolean): C {
for (element in this) if (predicate(element)) destination.add(element)
return destination
}
Copy the code
Similarly, it builds a temporary set to store the intermediate results of the operation, applying predicate conditions as it traverses the source set, and adding elements from the source set to the temporary set only when the conditions are met.
So the semantics of filter() are: retain only the set elements that satisfy the condition
toSet()
ToSet () was called before filter() :
public fun <T>可迭代<T>.toSet(a): Set<T> {
if (this is Collection) {
return when (size) {
0 -> emptySet()
1 -> setOf(if (this is List) this[0] else iterator().next())
else -> toCollection(LinkedHashSet<T>(mapCapacity(size)))
}
}
return toCollection(LinkedHashSet<T>()).optimizeReadOnlySet()
}
public fun <T, C : MutableCollection<in T>> Iterable<T>.toCollection(destination: C): C {
for (item in this) {
// Duplicate elements will fail to be added
destination.add(item)
}
return destination
}
Copy the code
The uniqueness of the element is realized with the help of LinkedHashSet while traversing the source set.
So the semantics of toSet() are: deiterate the collection elements
flatMap()
At the beginning of the call chain, flatMap() is called:
public inline fun <T, R>可迭代<T>.flatMap(transform: (T) - >可迭代<R>): List<R> {
return flatMapTo(ArrayList<R>(), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) - >可迭代<R>): C {
for (element in this) {
val list = transform(element)
destination.addAll(list)
}
return destination
}
Copy the code
The source code for flatMap() is very similar to map(), except that transform results in a collection type, which is then added to the temporary collection.
FlatMap () does two things: it transforms each element in the source set (which results in another set), and then merges multiple sets into a single set. This works well with data structures within collections, as if the student instances in this case are in a student list, and each student instance contains a list of courses. All courses in the student list can be easily tiled by the operation of first transformation and then tiling.
So the semantics of flatMap() are: spread out the inner sets within nested sets
asSequence()
Because every function that manipulates a set creates a temporary set to hold intermediate results.
Is there any way to get rid of temporary collection creation for better performance?
That’s why sequences are created. Rewrite the above code with sequences:
val friends = students.asSequence()
.flatMap { it.courses.asSequence() }
.filter { it.period < 70 && !it.isMust }
.map {
it.apply {
name = name.replace(name.first(), name.first().toUpperCase())
}
}
.sortedWith(compareBy({ it.period }, { it.name }))
.toSet()
Copy the code
The original set is transformed into a sequence by calling asSequence(), which divides operations on set elements into two categories:
- In the middle of operation
- At the end of the operating
In terms of return value, the middle operation returns another sequence, while the end operation returns a set (toSet() is the end operation).
In terms of execution timing, intermediate operations are lazy, meaning that intermediate operations are delayed. The end operation triggers the execution of all the delayed intermediate operations. So I moved toSet() to the end.
The sequence also changes the order in which the intermediate operations are executed, so if you don’t use the sequence, you have n intermediate operations that need to be traversed through the set n times, applying one operation at a time, and then using the sequence, you only have to traverse the set once, applying all the intermediate operations on each element at once.
To implement this set operation in Java, you would need to define a rather complicated algorithm and do some careful analysis to understand the business requirements, whereas Kotlin’s code is like translating the requirements into English and reading the code to understand the semantics. This kind of “read the meaning” effect, is really Java can’t match.
Summary of knowledge points
- through
data
With the keyword and the main constructor, Kotlin can declare a data class in a single line of code. - The primary constructor is a constructor used to assign an initial value to a class attribute. It does this by
constructor
Reserved words and class headers are declared on the same line. - Reserved words
vararg
Use to declare variadic arguments. Methods with variadic arguments can accept any number of arguments. - Can be achieved by
=
Set default values for parameters when declaring methods to reduce function overloading. - Can be achieved by
The variable name =
Syntax adds named parameters to method calls, increasing the readability of method calls. - The Kotlin standard library predefines a number of ways to work with collections, among them
filter()
The semantics of: retain only the set elements that satisfy the conditiontoSet()
Deduplicate the elements of the collectionflatMap()
To spread out an inner set within a nested setmap()
The semantics of the function are that a custom transformation is applied to each element of the collectionasSequence()
Used to sequence a series of collection operations to improve performance of collection operations.
Recommended reading
- Kotlin base | entrusted and its application
- Kotlin basic grammar | refused to noise
- Kotlin advanced | not variant, covariant and inverter
- Kotlin combat | after a year, with Kotlin refactoring a custom controls
- Kotlin combat | kill shape with syntactic sugar XML file
- Kotlin base | literal-minded Kotlin set operations
- Kotlin source | magic weapon to reduce the complexity of code
- Why Kotlin coroutines | CoroutineContext designed indexed set? (a)
- Kotlin advanced | the use of asynchronous data stream Flow scenarios