Source: author: mphone (joymufeng) my.oschina.net/joymufeng/blog/2251038

The question that has dogged the Scala community is why some Java developers are exalting Scala as the perfect language for a kiss from God; Other Java developers balk at it, finding it too complex to understand.

As a Java developer, I think there must be some misunderstanding as to why there are two different attitudes. Scala is a nugget of gold, but it’s so wrapped up in seemingly complex concepts or syntax that it’s hard to figure out its value in a short period of time.

At the same time, Java has been groping its way forward, but each step has been difficult because of Java’s heavy historical baggage. This article is aimed primarily at Java developers and aims to look at some of the Scala features that are most attractive to Java developers, starting with solving the actual problems in Java. Hopefully this will help you quickly find the ones that really touch you.

Value comparison

Teaser index: five stars

In Java, for reference types, == is used to compare reference equality, that is, to see if two reference variables refer to the same object. So a simple string comparison is very verbose:

String str = new String("Jack"); // If (STR == "Jack") {} // If (STR == "Jack"); = null && STR. Equals (" Jack ")) {} / / or abbreviated as if (" Jack ". The equals (STR)) {}

In Scala, where == is designed for value comparison, the underlying implementation calls the equals method on the object to the left of == and automatically handles the null case. So in Scala, you can safely use == for value comparisons:

val str = new String("Jack")
if (str == "Jack") {}

In everyday development, the need for value comparisons is much greater than for reference comparisons, so == is more intuitive for value comparisons. Of course, if you do need a reference comparison, Scala provides two methods: eq and ne:

val str1 = new String("Jack")
val str2 = new String("Jack")
if (str1 eq str2) {}

Type inference

Teaser index: Four stars

As we know, Scala is known for its strong type inference. Most of the time, we don’t need to care that the Scala type inference system exists, because most of the time its inference is intuitive. Java also added a proposal, JEP 286, in 2016 to introduce local-variable Type Inference for Java 10. With this feature, we can use var to define variables without explicitly declaring their types. A lot of people think this is an exciting feature, but let’s see what problems it causes before we get excited.

Conflicts with the Java 7 diamond operator

Java 7 introduced the diamond operator, which allows us to reduce redundant type information on the right side of expressions, such as:

List<Integer> numbers = new ArrayList<>();

If var is introduced, it will cause the type on the left to be lost, resulting in the type loss of the entire expression:

var numbers = new ArrayList<>();

So you have to choose between the var and diamond operators. You can’t have it both ways.

Error prone code

Here’s some Java code to check if the user exists:

public boolean userExistsIn(Set<Long> userIds) {
    var userId = getCurrentUserId();
    return userIds.contains(userId);
}

Take a close look at the above code. Can you see the problem at a glance? If getCurrentUserId() returns String, this method will always return false. If getCurrentUserId() returns String, this method will always return false. This is because the Set<Long>. Contains method accepts an argument of type Object. Some people may say, even if the type is explicitly declared, isn’t that helpful?

public boolean userExistsIn(Set<Long> userIds) {
    String userId = getCurrentUserId();
    return userIds.contains(userId);
}

The advantage of Java is its type readability. If you explicitly declare the type of the userId, it will still compile properly, but the error will be more likely to be detected during code review. This type of error is very likely to occur in Java because the getCurrentUserId() method is likely to change the return type due to refactoring, and the Java compiler will betray you at the critical moment by not reporting any compilation errors. Although this is due to the history of Java, the introduction of var has caused this error to spread.

Obviously, in Scala, this kind of low-level error cannot escape the compiler’s notice:

def userExistsIn(userIds: Set[Long]): Boolean = {
    val userId = getCurrentUserId()
    userIds.contains(userId)
}

If the userId is not of type Long, the above program will not compile.

String enhancement

Teaser index: Four stars

Common operations

Scala has enhanced the use of characters to provide more operations:

// remove "aabbcc". Distinct // "ABC" // remove first n characters, If n is greater than the string length, return the original string "abcd".take(10) // "abcd" // sort the string "bcad".sorted // "abcd" // filter the specified character "bcad".filter(_! = 'a') // "BCD" // Conversion "true".toboolean "123".toint "123.0".todouble

You can actually use a String as Seq[Char], and with Scala’s powerful collection operations, you can manipulate strings in any way you want.

Native string

In Scala, we can write native strings without escaping them, simply putting the contents of the string inside a pair of triple quotes:

Val s1= """Welcome here. Type "HELP" for HELP!" Val regex = """\d+"""

String interpolation

We can conveniently interpolate within a string by using the s-expression:

val name = "world"
val msg = s"hello, ${name}" // hello, world

Set operations

Teaser index: five stars

Scala’s collection design is one of the easiest things to get hooked on. It’s like a drug. With the collection operations Scala provides, we can do almost all of SQL’s work, which is one of the reasons Scala is a leader in big data.

Simple way to initialize

In Scala, we can initialize a list like this:

val list1 = List(1, 2, 3)

We can initialize a Map like this:

val map = Map("a" -> 1, "b" -> 2)

All collection types can be initialized in a similar way, simple and expressive.

Convenient Tuple type

Sometimes a method may return more than one value. Scala provides a Tuple type to temporarily store multiple values of different types while ensuring type safety. Don’t think you can do the same thing with a Tuple type using An Array type in Java. There is a fundamental difference. A Tuple explicitly declares the respective types of all elements, rather than a Java Array, where the element type is upconverted to the parent type of all elements. We can initialize a Tuple like this:

Val t = (" ABC ", 123, true) val s: String = t._1 // Take the first element val I: Int = T._2 // Take the second element val b: Boolean = T._3 // Take the third element

Note that the element index of a Tuple starts at 1.

The following example code looks for the maximum value in a list of long integers and returns the maximum value and its location:

def max(list: List[Long]): (Long, Int) = list.zipWithIndex.sorted.reverse.head

We convert the List[Long] to List[(Long, Int)] using the zipWithIndex method to get the index number of each element, and then sort, reverse, and first element, returning the maximum value and its location.

Chain calls

By chaining calls, we can focus on the processing and transformation of the data, rather than how to store and pass the data, and also avoid creating a lot of meaningless intermediate variables, greatly improving the readability of the program. In fact, the Max function above demonstrates chained calls. Let’s demonstrate how to use the set operation to implement the associated query function of SQL. The SQL statements to be implemented are as follows:

SELECT p.name, p.company, c.country FROM people p JOIN companies c ON p.companyId = c.id
WHERE p.age == 20

The above SQL statement implements an associative query of the people and companies tables, returning the name of all employees aged 20, age, and the name of the company they work for.

The corresponding Scala implementation code is as follows:

// Entity case class People(name: String, age: Int, companyId: String) case class Company(id: String, name: String) // Entity List val people = List(People("jack", 20, "0")) val companies = List(Company("0", FlatMap {p => companies. Filter (c => C.ID == p.companyId) . The map (c = > (p.n ame, p.age, citigroup ame))} / / results: List ((jack, 20, lightbend))

In fact, using a for expression looks a lot simpler:

for {
  p <- people if p.age == 20
  c <- companies if p.companyId == c.id
} yield (p.name, p.age, c.name)

Atypical collection operations

Scala’s collection operations are rich enough to fill a book with details. Here are just a few of the less common but very useful operations.

Go to:

List(1, 2, 2, 3).distinct // List(1, 2, 3)

Intersection:

Set(1, 2) & Set(2, 3)   // Set(2)

And set:

Set(1, 2) | Set(2, 3) // Set(1, 2, 3)

Difference set:

Set(1, 2) &~ Set(2, 3) // Set(1)

Alignment:

List(1, 2, 3).permutations.toList
//List(List(1, 2, 3), List(1, 3, 2), List(2, 1, 3), List(2, 3, 1), List(3, 1, 2), List(3, 2, 1))

Combination:

List(1, 2, 3).combinations(2).toList 
// List(List(1, 2), List(1, 3), List(2, 3))

Parallel collection

Scala’s parallel collections can take advantage of multi-core advantages to speed up the computation process, and by using the PAR approach on collections, we can transform the original collection into a parallel collection. The parallel set uses the dive-and-conquer algorithm to decompose the computing task into many subtasks, and then hand them to different threads for execution. Finally, the results of the computation are summarized. Here’s a simple example:

(1 to 10000).par.filter(i => i % 2 == 1).sum

Elegant value objects

Teaser index: five stars

Case Class

The Scala standard library includes a special Class called Case Class for modeling domain-level value objects. The good thing about this is that all the default behaviors are properly designed, right out of the box. Here we define a User value object using the Case Class:

case class User(name: String, role: String = "user", addTime: Instant = Instant.now())

The User class is defined in a single line of code, just imagine the Java implementation.

A concise way to instantiate

We define default values for the role and addTime attributes, so we can create a User instance using just name:

val u = User("jack")

When creating an instance, we can also change the default values with named parameter syntax:

val u = User("jack", role = "admin")

In real life development, a model class or value object might have many properties, many of which could be set to a reasonable default value. With default values and named parameters, it is very easy to create instances of model classes and value objects. Therefore, there is little need to create objects using the factory or constructor pattern in Scala. If the creation process of an object is really complicated, it can be created in a companion object, such as:

object User {
  def apply(name: String): User = User(name, "user", Instant.now())
}

You can omit the method name apply when creating an instance using a companion object method, for example:

User("jack") // Is equivalent to user.apply ("jack")

In this example, the code that instantiates the object using the companion object method is exactly the same as the code above that uses the class constructor. The compiler will preferentially select the apply method of the companion object.

immutability

By default, instances of a Case Class are immutable, meaning they can be shared arbitrarily and are accessed concurrently without synchronization, saving valuable memory. In Java, objects need to be deeply copied when they are shared, otherwise changes in one place will affect others. For example, we define a Role object in Java:

public class Role { public String id = ""; public String name = "user"; public Role(String id, String name) { this.id = id; this.name = name; }}

Problems arise if Role instances are shared between two users, as follows:

u1.role = new Role("user", "user");
u2.role = u1.role;

When we change u1.role, u2 is affected. The Java solution is to either deep clone a new object based on u1.role, or create a new role object and assign it to U2.

Copy the object

In Scala, since a Case Class is immutable, what if you want to change its value? It’s easy to copy a new immutable object with named parameters:

val u1 = User("jack")
val u2 = u1.copy(name = "role", role = "admin")

Clear debugging information

We don’t need to write extra code to get clear debugging information, such as:

val users = List(User("jack"), User("rose"))
println(users)

The output is as follows:

List (User (jack, the User, the 2018-10-20 T13:03:16. 170 z), User (rose, the User, the 2018-10-20 T13:03:16. 170 z))

By default, values are used to compare equality

In Scala, value comparisons are used by default instead of reference comparisons, which are more intuitive to use:

User("jack") == User("jack") // true

The above value comparison is done out of the box without overriding the hashCode and equals methods.

Pattern matching

Teaser index: five stars

Better readability

When your code has multiple if branches and nested if’s, the readability of your code is greatly reduced. Using pattern matching in Scala can easily solve this problem. The following code demonstrates a currency type match:

sealed trait Currency case class Dollar(value: Double) extends Currency case class Euro(value: Double) extends Currency val Currency = ... Currency match {case Dollar(v) => "$" + v case Euro(v) => "€" + v case _ => "unknown"}

We can also make some complex matches, and we can add if to the match:

use match {
    case User("jack", _, _) => ...
    case User(_, _, addTime) if addTime.isAfter(time) => ...
    case _ => ...
}

Variable assignment

Using pattern matching, we can quickly extract the value of a specific section and complete the variable definition. We can assign values from a Tuple directly to variables:

Val (name, role, addTime) = tuple val (name, role, addTime) = tuple

The same is true for Case classes:

Val User(name, role, addTime) = User("jack") val User(name, role, addTime) = User("jack"

Concurrent programming

Teaser index: five stars

In Scala, we only care about the business logic when writing concurrent code, not how the task executes. We can pass in a thread pool explicitly or implicitly, and the execution is done by the thread pool. A Future is used to start an asynchronous task and save the results. We can use the for expression to collect the results of multiple Futures to avoid callback hell:

val f1 = Future{ 1 + 2 }
val f2 = Future{ 3 + 4 }
for {
    v1 <- f1
    v2 <- f2
}{
    println(v1 + v2) // 10
}

If you want to crawl 100 pages at once, a single line of code will do:

Future.sequence(urls.map(url => http.get(url))).foreach{ contents => ... }

The future. sequence method is used to collect the results of all Future executions. The foreach method allows us to retrieve the collected results for subsequent processing.

When we want to implement fully asynchronous request flow limiting, we need fine-grained control over the execution timing of each Future. That means we need a switch that controls the Future, and yes, that switch is a Promise. Each Promise instance has a unique Future associated with it:

Val p = Promise[Int]() val f = p.future for (v < -f) {println(v)} p.success(3)

Cross-thread error handling

Java handles errors through an exception mechanism, but the problem is that Java code can only catch exceptions in the current thread, not across threads. In Scala, we can use a Future to catch exceptions that occur in any thread. Asynchronous tasks can succeed or fail, so we need a data type that can indicate either success or failure, which in Scala is Try[T]. Try[T] has two subtypes, Success[T] indicating Success and Failure[T] indicating Failure. Just like Schrodinger’s cat in quantum physics, you have no way of knowing whether the result will be Success[T] or Failure[T] until the asynchronous task is completed.

Val f = Future{/* asynchronous task */} // Get match {case Success(v) => // Failure(t) => // Failure(t)}

We can also make a Future recover from errors:

Val f = Future{/* asynchronous task */} for{result < -f => {case t => /* error */}} yield {// result}

Declarative programming

Teaser index: Four stars

Scala encourages declarative programming, and code written declaratively is more readable. In contrast to traditional imperative programming, declarative programming focuses more on what I want to do than how I want to do it. For example, we often implement paging operations that return 10 pieces of data per page:

val allUsers = List(User("jack"), User("rose")) val pageList = allUsers .sortBy(u => (u.role, u.name, Drop (page * 10) // skip the previous page data. Take (10) // fetch the current page data, if there are less than 10, return all

You just have to tell Scala what to do, such as sort by role first, by name if roles are the same, and addTime if roles and names are the same. The underlying concrete sorting implementation is already encapsulated and the developer does not need to implement it. In addition, declarative code is more suitable for parallel optimization because it specifies only the pattern to be satisfied by the result, not the exact procedure to execute. Since imperative code specifies detailed steps and sequence of execution, it can only rely on higher clock frequency to improve execution speed.

Expression oriented programming

Teaser index: Four stars

In Scala, everything is an expression, including common control structures like if, for, while, and so on. The difference between expressions and statements is that each expression has an explicit return value.

val i = if(true){ 1 } else { 0 } // i = 1
val list1 = List(1, 2, 3)
val list2 = for(i <- list1) yield { i + 1 }

Different expressions can be combined to form a larger expression, where pattern matching can be powerful. Let’s illustrate this with an interpreter that calculates addition.

An integer addition interpreter

Let’s first define the basic expression type:

abstract class Expr
case class Number(num: Int) extends Expr
case class PlusExpr(left: Expr, right: Expr) extends Expr

The above defines two expression types: Number for an integer expression and PlusExpr for an additive expression. Here we evaluate the expression based on pattern matching:

def evalExpr(expr: Expr): Int = {
  expr match {
    case Number(n) => n
    case PlusExpr(left, right) => evalExpr(left) + evalExpr(right)
  }
}

Let’s try to evaluate a larger expression:

evalExpr(PlusExpr(PlusExpr(Number(1), Number(2)), PlusExpr(Number(3), Number(4)))) // 10

Implicit parameters and implicit conversions

Teaser index: five stars

Implicit parameter

Wouldn’t you get annoyed if you had to explicitly pass in a thread pool parameter every time you wanted to perform an asynchronous task? Scala takes this hassle out of you with implicit arguments. For example, when a Future creates an asynchronous task with an implicit parameter of type ExecutionContext, the compiler will automatically look for an appropriate ExecutionContext in the current scope. If not, it will report a compilation error:

implicit val ec: ExecutionContext = ??? Val f = Future {/* Asynchronous task */}

We can also explicitly pass the ExecutionContext argument, specifying the pool of threads to be used:

implicit val ec: ExecutionContext = ??? Val f = Future {/* asynchronous task */}(ec)

Implicit conversion

Implicit conversions are more flexible to use than implicit parameters. If Scala finds an error at compile time, an implicit conversion rule is applied to the error code before the error is reported, and if the rule is applied to make it compile, an implicit conversion has been completed successfully.

Seamless docking between different libraries

When the passed parameter type does not match the target type, the compiler attempts an implicit conversion. Using this feature, we seamlessly connect existing data types to the three-party libraries. For example, we want to use MongoDB’s official Java driver to perform database query operations in Scala project, but the query interface accepts parameter types of BsonDocument, because using BsonDocument to build the query is awkward. We want to be able to use Scala’s JSON library to build a query object, and then directly pass it to the query interface of the official driver, without changing any code of the official driver. Implicit conversion can be very easy to achieve this function:

implicit def toBson(json: JsObject): BsonDocument = ... Val json: JsObject = json.obj ("_id" -> "0") jcollection.find (json) // The compiler will automatically call toBson(json)

Implicit conversions allow us to seamlessly connect our data types to the three-party library code without changing it. For example, we implemented an implicit transformation that seamlessly interconnects Scala’s JsObject type into MongoDB’s official Java driver’s query interface, making it look like the official MongoDB driver actually provides this interface.

It is also possible to seamlessly integrate data types from the three-party library into the existing interface by implementing an implicit conversion method.

Extend the functionality of an existing class

For example, we define a Dollar currency type, Dollar:

class Dollar(value: Double) {
  def + (that: Dollar): Dollar = ...
  def + (that: Int): Dollar = ...
}

So we can do the following:

Val halfDollar = new Dollar(0.5) halfDollar + halfDollar // 1 Dollar halfDollar + 0.5 // 1 Dollar

However, we couldn’t perform an operation like 0.5 + halfDollar because we couldn’t find a suitable + method on Double.

In Scala, all we need to do is implement a simple implicit transformation:

Implicit def doubleToDollar(d: Double) = new Dollar(d) 0.5 + halfDollar

Better runtime performance

In daily development, we often need to convert value objects into Json format to facilitate data transfer. The usual practice in Java is to use reflection, but we know that using reflection comes at a cost, in the form of runtime performance overhead. Scala, on the other hand, generates implicit Json codec objects for value objects at compile time. These codec objects are just plain function calls that do not involve any reflection operations, which greatly improves the runtime performance of the system.

summary

If you’ve made it this far, I’ll be glad to know that some of Scala’s features have already attracted you. But there’s more to Scala than that, and these are just some of the features that catch your eye the most. If you are willing to open the door to Scala, you will see a very different programming world.

Recent hot articles recommended:

1.1,000+ Java interview questions and answers (the latest version of 2021)

2. Finally got the IntelliJ IDEA activation code through the open source project, how sweet!

3. Ali Mock tool officially open source, kill all the Mock tools on the market!

4.Spring Cloud 2020.0.0 is officially released, a new and disruptive version!

5. “Java Development Manual (Songshan version)” the latest release, speed download!

Feel good, don’t forget to click “like” + forward oh!