• Java和Scala语法比较


    类型推断

    挑逗指数: 四星

    我们知道,Scala一向以强大的类型推断闻名于世。很多时候,我们无须关心Scala类型推断系统的存在,因为很多时候它推断的结果跟直觉是一致的。 Java在2016 年也新增了一份提议JEP 286,计划为Java 10引入局部变量类型推断(Local-Variable Type Inference)。利用这个特性,我们可以使用var定义变量而无需显式声明其类型。很多人认为这是一项激动人心的特性,但是高兴之前我们要先看看它会为我们带来哪些问题。

    与Java 7的钻石操作符冲突

    Java 7引进了钻石操作符,使得我们可以降低表达式右侧的冗余类型信息,例如:

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

    如果引入了var,则会导致左侧的类型丢失,从而导致整个表达式的类型丢失:

    var numbers = new ArrayList<>();

    所以var和钻石操作符必须二选一,鱼与熊掌不可兼得。

    容易导致错误的代码

    下面是一段检查用户是否存在的 Java 代码:

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

    请仔细观察上述代码,你能一眼看出问题所在吗? userId的类型被var隐去了,如果getCurrentUserId()返回的是String类型,上述代码仍然可以正常通过编译,却无形中埋下了隐患,这个方法将会永远返回false, 因为Set<Long>.contains方法接受的参数类型是Object。可能有人会说,就算显式声明了类型,不也是于事无补吗?

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

    Java的优势在于它的类型可读性,如果显式声明了userId的类型,虽然还是可以正常通过编译,但是在代码审查时,这个错误将会更容易被发现。 这种类型的错误在Java中非常容易发生,因为getCurrentUserId()方法很可能因为重构而改变了返回类型,而Java编译器却在关键时刻背叛了你,没有报告任何的编译错误,只会报警告。 虽然这是由于Java的历史原因导致的,但是由于var的引入,会导致这个错误不断的蔓延。

    很显然,在Scala中,这种低级错误是无法逃过编译器法眼的:

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

    如果userId不是Long类型,则上面的程序无法通过编译。

    字符串增强

    挑逗指数: 四星

    常用操作

    Scala针对字符作进行了增强,提供了更多的使用操作:

    //字符串去重
    "aabbcc".distinct // "abc"
    //取前n个字符,如果n大于字符串长度返回原字符串
    "abcd".take(10) // "abcd"
    //字符串排序
    "bcad".sorted // "abcd"
    //过滤特定字符
    "bcad".filter(_ != 'a') // "bcd"
    //类型转换
    "true".toBoolean
    "123".toInt
    "123.0".toDouble

    其实你完全可以把String当做Seq[Char]使用,利用Scala强大的集合操作,你可以随心所欲地操作字符串。

    原生字符串

    在Scala中,我们可以直接书写原生字符串而不用进行转义,将字符串内容放入一对三引号内即可:

    //包含换行的字符串
    val s1= """Welcome here.
     Type "HELP" for help!"""
     
    //包含正则表达式的字符串 
    val regex = """d+""" 

    字符串插值

    通过s表达式,我们可以很方便地在字符串内插值:

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

    集合操作

    挑逗指数: 五星

    Scala的集合设计是最容易让人着迷的地方,就像毒品一样,一沾上便让人深陷其中难以自拔。通过Scala提供的集合操作,我们基本上可以实现SQL的全部功能,这也是为什么Scala能够在大数据领域独领风骚的重要原因之一。

    简洁的初始化方式

    在 Scala中,我们可以这样初始化一个列表:

    val list1 = List(1, 2, 3)

    可以这样初始化一个 Map:

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

    所有的集合类型均可以用类似的方式完成初始化,简洁而富有表达力。

    便捷的Tuple类型

    有时方法的返回值可能不止一个,Scala提供了Tuple(元组)类型用于临时存放多个不同类型的值,同时能够保证类型安全性。千万不要认为使用Java的Array类型也可以同样实现Tuple类型的功能,它们之间有着本质的区别。Tuple会显式声明所有元素的各自类型,而不是像Java Array那样,元素类型会被向上转型为所有元素的父类型。

    我们可以这样初始化一个Tuple:

    val t = ("abc", 123, true)
    val s: String = t._1 // 取第1个元素
    val i: Int = t._2 // 取第2个元素
    val b: Boolean = t._3 // 取第3个元素

    需要注意的是Tuple的元素索引从1开始。

    下面的示例代码是在一个长整型列表中寻找最大值,并返回这个最大值以及它所在的位置:

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

    我们通过zipWithIndex方法获取每个元素的索引号,从而将List[Long]转换成了List[(Long, Int)],然后对其依次进行排序、倒序和取首元素,最终返回最大值及其所在位置。

    链式调用

    通过链式调用,我们可以将关注点放在数据的处理和转换上,而无需考虑如何存储和传递数据,同时也避免了创建大量无意义的中间变量,大大增强程序的可读性。其实上面的max函数已经演示了链式调用。下面这段代码演示了如果在一个整型列表中寻找大于3的最小奇数:

    val list = List(3, 6, 4, 1, 7, 8)
    list.filter(i => i % 2 == 1).filter(i => i > 3).sorted.head

    非典型集合操作

    Scala的集合操作非常丰富,如果要详细说明足够写一本书了。这里仅列出一些不那么常用但却非常好用的操作。

    去重:

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

    交集:

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

    并集:

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

    差集:

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

    排列:

    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))

    组合:

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

    并行集合

    Scala的并行集合可以利用多核优势加速计算过程,通过集合上的par方法,我们可以将原集合转换成并行集合。并行集合利用分治算法将计算任务分解成很多子任务,然后交给不同的线程执行,最后将计算结果进行汇总。下面是一个简单的示例:

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

    优雅的值对象

    挑逗指数: 五星

    Case Class

    Scala标准库包含了一个特殊的Class叫做Case Class,专门用于领域层值对象的建模。它的好处是所有的默认行为都经过了合理的设计,开箱即用。下面我们使用 Case Class 定义了一个User值对象:

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

    仅仅一行代码便完成了User类的定义,请脑补一下Java的实现。

    简洁的实例化方式

    我们为role和addTime两个属性定义了默认值,所以我们可以只使用name创建一个User实例:

    val u = User("jack")

    在创建实例时,我们也可以命名参数(named parameter)语法改变默认值:

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

    在实际开发中,一个模型类或值对象可能拥有很多属性,其实很多属性都可以设置一个合理的默认值。利用默认值和命名参数,我们可以非常方便地创建模型类和值对象的实例。 所以在 Scala 中基本上不需要使用工厂模式或构造器模式创建对象,如果对象的创建过程确实非常复杂,则可以放在伴生对象中创建,例如:

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

    在使用伴生对象方法创建实例时可以省略方法名 apply,例如:

    User("jack") // 等价于 User.apply("jack")

    在这个例子里,使用伴生对象方法实例化对象的代码,与上面使用类构造器的代码完全一样,编译器会优先选择伴生对象的apply方法。

    不可变性

    Case Class 在默认情况下实例是不可变的,意味着它可以被任意共享,并发访问时也无需同步,大大地节省了宝贵的内存空间。而在 Java 中,对象被共享时需要进行深拷贝,否则一个地方的修改会影响到其它地方。例如在 Java 中定义了一个 Role 对象:

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

    如果在两个User之间共享Role实例就会出现问题,就像下面这样:

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

    当我们修改u1.role时,u2就会受到影响,Java的解决方式是要么基于u1.role深度克隆一个新对象出来,要么新创建一个Role对象赋值给u2。

    对象拷贝

    在 Scala 中,既然 Case Class 是不可变的,那么如果想改变它的值该怎么办呢?其实很简单,利用命名参数可以很容易拷贝一个新的不可变对象出来:

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

    清晰的调试信息

    我们不需要编写额外的代码便可以得到清晰的调试信息,例如:

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

    输出内容如下:

    List(User(jack,user,2018-10-20T13:03:16.170Z), User(rose,user,2018-10-20T13:03:16.170Z))

    默认使用值比较相等性

    在Scala中,默认采用值比较而非引用比较,使用起来更加符合直觉:

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

    上面的值比较是开箱即用的,无需重写hashCode和equals方法。

    模式匹配

    挑逗指数: 五星

    更强的可读性

    当你的代码中存在多个if分支并且if之间还会有嵌套,那么代码的可读性将会大大降低。而在Scala中使用模式匹配可以很容易地解决这个问题,下面的代码演示货币类型的匹配:

    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"
    }

    我们也可以进行一些复杂的匹配,并且在匹配时可以增加if判断:

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

    变量赋值

    利用模式匹配,我们可以快速提取特定部分的值并完成变量定义。 我们可以将Tuple中的值直接赋值给变量:

    val tuple = ("jack", "user", Instant.now())
    val (name, role, addTime) = tuple
    // 变量 name, role, addTime 在当前作用域内可以直接使用

    对于Case Class也是一样:

    val User(name, role, addTime) = User("jack")
    // 变量 name, role, addTime 在当前作用域内可以直接使用

    并发编程

    挑逗指数: 五星

    在Scala中,我们在编写并发代码时只需要关心业务逻辑即可,而不需要关注任务如何执行。我们可以通过显式或隐式方式传入一个线程池,具体的执行过程由线程池完成。Future用于启动一个异步任务并且保存执行结果,我们可以用for表达式收集多个Future的执行结果,从而避免回调地狱:

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

    使用Future开发爬虫程序将会让你事半功倍,假如你想同时抓取100个页面数据,一行代码就可以了:

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

    Future.sequence方法用于收集所有Future的执行结果,通过foreach方法我们可以取出收集结果并进行后续处理。

    当我们要实现完全异步的请求限流时,就需要精细地控制每个Future的执行时机。也就是说我们需要一个控制Future的开关,没错,这个开关就是Promise。每个Promise实例都会有一个唯一的Future与之相关联:

    val p = Promise[Int]()
    val f = p.future
    for (v <- f) { println(v) } // 3秒后才会执行打印操作
    //3秒钟之后返回3
    Thread.sleep(3000)
    p.success(3)

    跨线程错误处理

    Java通过异常机制处理错误,但是问题在于Java代码只能捕获当前线程的异常,而无法跨线程捕获异常。而在Scala中,我们可以通过Future捕获任意线程中发生的异常。

    异步任务可能成功也可能失败,所以我们需要一种既可以表示成功,也可以表示失败的数据类型,在Scala中它就是Try[T]。Try[T]有两个子类型,Success[T]表示成功,Failure[T]表示失败。就像量子物理学中薛定谔的猫,在异步任务执行之前,你根本无法预知返回的结果是Success[T] 还是Failure[T],只有当异步任务完成执行以后结果才能确定下来。

    val f = Future{ /*异步任务*/ } 
    // 当异步任务执行完成时
    f.value.get match {
     case Success(v) => // 处理成功情况
     case Failure(t) => // 处理失败情况
    }

    我们也可以让一个Future从错误中恢复:

    val f = Future{ /*异步任务*/ }
    for{
     result <- f.recover{ case t => /*处理错误*/ }
    } yield {
     // 处理结果
    }

    声明式编程

    挑逗指数: 四星

    Scala鼓励声明式编程,采用声明式编写的代码可读性更强。与传统的过程式编程相比,声明式编程更关注我想做什么而不是怎么去做。例如我们经常要实现分页操作,每页返回10条数据:

    val allUsers = List(User("jack"), User("rose"))
    val pageList = 
     allUsers
     .sortBy(u => (u.role, u.name, u.addTime)) // 依次按 role, name, addTime 进行排序
     .drop(page * 10) // 跳过之前页数据
     .take(10) // 取当前页数据,如不足10个则全部返回

    你只需要告诉Scala要做什么,比如说先按role排序,如果role相同则按name排序,如果role和name都相同,再按addTime排序。底层具体的排序实现已经封装好了,开发者无需实现。

    面向表达式编程

    挑逗指数: 四星

    在Scala中,一切都是表达式,包括if, for, while等常见的控制结构均是表达式。表达式和语句的不同之处在于每个表达式都有明确的返回值。

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

    不同的表达式可以组合在一起形成一个更大的表达式,再结合上模式匹配将会发挥巨大的威力。下面我们以一个计算加法的解释器来做说明。

    一个整数加法解释器

    我们首先定义基本的表达式类型:

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

    上面定义了两个表达式类型,Number表示一个整数表达式, PlusExpr表示一个加法表达式。

    下面我们基于模式匹配实现表达式的求值运算:

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

    我们来尝试针对一个较大的表达式进行求值:

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

    隐式参数和隐式转换

    挑逗指数: 五星

    隐式参数

    如果每当要执行异步任务时,都需要显式传入线程池参数,你会不会觉得很烦?Scala通过隐式参数为你解除这个烦恼。例如Future在创建异步任务时就声明了一个 ExecutionContext类型的隐式参数,编译器会自动在当前作用域内寻找合适的ExecutionContext,如果找不到则会报编译错误:

    implicit val ec: ExecutionContext = ???
    val f = Future { /*异步任务*/ }

    当然我们也可以显式传递ExecutionContext参数,明确指定使用的线程池:

    implicit val ec: ExecutionContext = ???
    val f = Future { /*异步任务*/ }(ec)

    隐式转换

    隐式转换相比较于隐式参数,使用起来更来灵活。如果Scala在编译时发现了错误,在报错之前,会先对错误代码应用隐式转换规则,如果在应用规则之后可以使得其通过编译,则表示成功地完成了一次隐式转换。

    在不同的库间实现无缝对接

    当传入的参数类型和目标类型不匹配时,编译器会尝试隐式转换。利用这个功能,我们将已有的数据类型无缝对接到三方库上。例如我们想在Scala项目中使用 MongoDB的官方Java驱动执行数据库查询操作,但是查询接口接受的参数类型是BsonDocument,由于使用BsonDocument构建查询比较笨拙,我们希望能够使用Scala的JSON库构建一个查询对象,然后直接传递给官方驱动的查询接口,而无需改变官方驱动的任何代码,利用隐式转换可以非常轻松地实现这个功能:

    implicit def toBson(json: JsObject): BsonDocument = ...
    val json: JsObject = Json.obj("_id" -> "0")
    jCollection.find(json) // 编译器会自动调用 toBson(json)

    利用隐式转换,我们可以在不改动三方库代码的情况下,将我们的数据类型与其进行无缝对接。例如我们通过实现一个隐式转换,将Scala的JsObject类型无缝地对接到了MongoDB的官方Java驱动的查询接口中,看起就像是MongoDB官方驱动真的提供了这个接口一样。

    同时我们也可以将来自三方库的数据类型无缝集成到现有的接口中,也只需要实现一个隐式转换方法即可。

    扩展已有类的功能

    例如我们定义了一个美元货币类型 Dollar:

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

    于是我们可以执行如下操作:

    val halfDollar = new Dollar(0.5)
    halfDollar + halfDollar // 1 dollar
    halfDollar + 0.5 // 1 dollar

    但是我们却无法执行像 0.5 + halfDollar这样的运算,因为在Double类型上无法找到一个合适的 + 方法。

    在Scala中,为了实现上面的运算,我们只需要实现一个简单的隐式转换就可以了:

    implicit def doubleToDollar(d: Double) = new Dollar(d)
    0.5 + halfDollar // 等价于 doubleToDollar(0.5) + halfDollar

    更好的运行时性能

    在日常开发中,我们通常需要将值对象转换成Json格式以方便数据传输。Java的通常做法是使用反射,但是我们知道使用反射是要付出代价的,要承受运行时的性能开销。而Scala则可以在编译时为值对象生成隐式的Json编解码器,这些编解码器只不过是普通的函数调用而已,不涉及任何反射操作,在很大程度上提升了系统的运行时性能。

    本文转自:https://www.toutiao.com/a6615517427591545357/?tt_from=mobile_qq&utm_campaign=client_share&timestamp=1540305875&app=news_article&utm_source=mobile_qq&iid=47022314370&utm_medium=toutiao_android&group_id=6615517427591545357

  • 相关阅读:
    每日一题:用一个SQL语句交换两条数据某一列的值
    String value(); String[] value(); 比较
    java.sql.SQLException: Unknown system variable 'tx_isolation'
    return; 用法
    Linux和UNIX的关系及区别(详解版)
    404
    Centos 7 修改YUM镜像源地址为阿里云镜像地址
    JVM面试必备
    秒杀优化迭代
    电商秒杀基础构建项目笔记2(优化效验准则和商品模型创建)
  • 原文地址:https://www.cnblogs.com/nizuimeiabc1/p/9844389.html
Copyright © 2020-2023  润新知