一直困扰Scala社区的一个问题是,为什么一些Java开发人员将Scala奉为上帝之吻的完美语言?它对此犹豫不决,认为它太复杂了,难以理解。同样是Java开发者,为什么会有两种截然不同的态度,我想一定是有误区。Scala是一粒金子,但它被一些表面上复杂的概念或语法包裹得严严实实,人们很难在短时间内弄清楚它的价值。与此同时,Java也在不断摸索前行,但由于Java背负着沉重的历史包袱,每前进一步都异常艰难。本文主要面向Java开发者,希望从解决Java实际问题的角度出发,整理出一些最有可能吸引Java开发者的Scala特性。希望它能帮助你快速找到那些真正能打动你的点。类型推断挑衅指数:四颗星我们知道,Scala一直以其强大的类型推断而著称。很多时候,我们不需要关心Scala的类型推断系统是否存在,因为很多时候它的推断结果是符合直觉的。Java在2016年还增加了一个提案JEP286,计划在Java10中引入Local-VariableTypeInference。利用这个特性,我们可以使用var来定义变量,而无需显式声明它们的类型。许多人认为这是一个令人兴奋的功能,但在我们高兴之前,我们必须看看它会给我们带来什么问题。与Java7的菱形运算符冲突Java7引入了菱形运算符,可以让我们减少表达式右侧的冗余类型信息,例如:Listnumbers=newArrayList<>();如果引入var,会导致左边表达式的类型丢失,从而导致整个表达式的类型丢失:varnumbers=newArrayList<>();所以var和diamond运算符必须两者选其一,不能两者兼得。容易出错的代码下面是一段检查用户是否存在的Java代码:publicbooleanuserExistsIn(SetuserIds){varuserId=getCurrentUserId();returnuserIds.contains(userId);}请仔细观察上面的代码,一眼就能看出是什么问题?userId的类型被var隐藏了。如果getCurrentUserId()返回的是String类型,上面的代码还是可以正常编译的,但是隐患就隐藏了。此方法将始终返回false,因为Set.contains方法接受的参数类型是Object。可能有人会说,就算显式声明了类型也没用?publicbooleanuserExistsIn(SetuserIds){StringuserId=getCurrentUserId();returnuserIds.contains(userId);}Java的优点是它的类型具有可读性,如果显式声明userId的类型,虽然仍然可以正常编译,在代码审查时会更容易发现这个错误。这种错误在Java中是非常容易发生的,因为getCurrentUserId()方法很可能因为重构而改变了返回类型,Java编译器在关键时刻背叛了你,不报任何编译错误。虽然这是Java的历史原因,但由于var的引入,这个错误还是会继续蔓延。显然,在Scala中,这样的低级错误逃不过编译器的眼睛:defuserExistsIn(userIds:Set[Long]):Boolean={valuserId=getCurrentUserId()userIds.contains(userId)}ifuserIdisnotoftypeLong,上面的程序无法编译。字符串增强预告索引:四星常用操作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中,我们可以不转义直接写原生字符串,只需要将字符串内容放在一对三重引号中即可://Stringscontainingnewlinesvals1="""Welcomehere.Type"HELP"forhelp!"""//A包含正则表达式的字符串valregex="""\d+"""字符串插值通过s表达式,我们可以很方便的在字符串中进行插值:valname="world"valmsg=s"hello,${name}"//hello,世界布景操作预告指数:五星级Scala的布景设计是最让人着迷的地方,就像毒品一样,一旦得到,便会深深地无法自拔。通过Scala提供的集合操作,我们基本可以实现SQL的所有功能,这也是Scala能够称霸大数据领域的重要原因之一。简单的初始化方法在Scala中,我们可以这样初始化一个列表:vallist1=List(1,2,3)和初始化一个Map这样:valmap=Map("a"->1,"b"->2)all的集合类型可以用类似的方式初始化,简洁明了。方便的元组类型有时方法可能返回多个值。Scala提供了Tuple(元组)类型来临时存储多个不同类型的值,同时保证类型安全。不要以为用Java的Array类型也能实现Tuple类型的功能。它们之间有着本质的区别。Tuple会显式地声明所有元素各自的类型,而不是像JavaArray那样向上转型为所有元素的父类型。我们可以这样初始化一个元组:valt=("abc",123,true)vals:String=t._1//取第一个元素vali:Int=t._2//取第二个元素valb:Boolean=t._3//取第三个元素。需要注意的是,Tuple的元素索引是从1开始的。下面的示例代码是在一个长整数列表中寻找最大值,并返回最大值及其位置:defmax(list:List[Long]):(Long,Int)=list.zipWithIndex.sorted.reverse.head我们通过zipWithIndex方法获取每个元素的索引号,从而将List[Long]转化为List[(Long,Int)],然后进行排序,倒序依次取第一个元素,最后返回最大值及其位置。链式调用通过链式调用,我们可以专注于数据的处理和转换,而无需考虑如何存储和传递数据,避免创建大量无意义的中间变量,大大增强了程序的可读性。其实上面的max函数已经演示了链式调用。以下代码演示了如何在整数列表中找到大于3的最小奇数:vallist=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方法,我们可以将原始集合转化为并行集合。并行集合使用分而治之的算法,将计算任务分解成很多子任务,然后交给不同的线程执行,最后汇总计算结果。下面是一个简单的例子:(1to10000).par.filter(i=>i%2==1).sum优雅值对象挑衅指数:五星级CaseClassScala标准库中包含一个特殊的Class,叫做CaseClass,它用于在领域层中对值对象进行建模。它的好处是所有默认行为都经过精心设计并且开箱即用。下面我们使用Case类来定义一个User类的值对象:caseclassUser(name:String,role:String="user",addTime:Instant=Instant.now())一行代码就完成了User类的定义,请大家集思广益我们来看看Java的实现。简洁的实例化方式我们已经为role和addTime这两个属性定义了默认值,所以我们可以创建一个只有名字的User实例:value=User("jack")在创建实例的时候,我们也可以命名参数(namedparameter)更改默认值的语法:value=User("jack",role="admin")在实际开发中,一个模型类或值对象可能有很多属性,其实很多属性都可以通过合理的设置默认值。使用默认值和命名参数,我们可以轻松创建模型类和值对象的实例。因此,在Scala中,基本上不需要使用工厂模式或构造器模式来创建对象。如果对象创建过程实在复杂,可以在伴生对象中创建,例如:objectUser{defapply(name:String):User=User(name,"user",Instant.now())}方法名使用伴生对象方法创建实例时可以省略apply,例如:User("jack")//等价于User.apply("jack")本例中使用伴生对象实例化对象的代码方法与上面使用类构造函数的代码完全相同,编译器会优先考虑伴生对象的apply方法。不可变性CaseClass的实例默认是不可变的,也就是说可以任意共享,并发访问时不需要同步,大大节省了宝贵的内存空间。在Java中,共享对象时需要深拷贝,否则一个地方的改变会影响到其他地方。比如Java中定义了一个Role对象:publicclassRole{publicStringid="";publicStringname="user";publicRole(Stringid,Stringname){this.id=id;this.name=name;}}如果两个User之间有之间共享Role实例时会出现问题,如下:u1.role=newRole("user","user");u2.role=u1.role;当我们修改u1.role时,u2会受到影响,Java的解决方案是要么在u1.role的基础上深度克隆一个新的对象,要么创建一个新的Role对象赋值给u2。对象复制在Scala中,由于CaseClass是不可变的,如果我想改变它的值怎么办?其实很简单。使用命名参数很容易复制一个新的不可变对象:value1=User("jack")valu2=u1.copy(name="role",role="admin")清除调试信息编写额外的代码,例如:valusers=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方法。PatternMatchingProvocativeIndex:五星级更强的可读性当你的代码中有多个if分支,并且if之间会嵌套时,代码的可读性会大大降低。在Scala中使用模式匹配可以很容易地解决这个问题。以下代码演示了货币类型匹配:sealedtraitCurrencycaseclassDollar(value:Double)extendsCurrencycaseclassEuro(value:Double)extendsCurrencyvalCurrency=...currencymatch{caseDollar(v)=>"$"+vcaseEuro(v)=>""+vcase_=>"unknown"}我们也可以做一些复杂的匹配,匹配的时候可以加上if判断:usematch{caseUser("jack",_,_)=>...caseUser(_,_,addTime)ifaddTime.isAfter(time)=>...case_=>...}变量赋值使用模式匹配,我们可以快速提取特定部分的值并完成变量定义。我们可以直接将Tuple中的值赋给变量:valtuple=("jack","user",Instant.now())val(name,role,addTime)=tuple//变量name,role,addTime中currentrole在scope中可以直接使用,CaseClass也是如此:valUser(name,role,addTime)=User("jack")//变量name,role,addTime在当前scope中可以直接使用并发编程挑衅指数:Scala中的五颗星,在编写并发代码时,我们只需要关心业务逻辑,而不需要关心任务是如何执行的。我们可以显式或隐式传入一个线程池,具体的执行过程由线程池完成。Future用于启动异步任务并保存执行结果。我们可以使用for表达式收集多个Future的执行结果来避免回调地狱:valf1=Future{1+2}valf2=Future{3+4}for{v1<-f1v2<-f2}{println(v1+v2)//10}用Future开发爬虫程序,让你事半功倍。如果你想同时抓取100页数据,一行代码就够了:Future.sequence(Theurls.map(url=>http.get(url))).foreach{contents=>...}Future.sequence方法用于收集所有Futures的执行结果。通过foreach方法,我们可以获取采集结果并进行后续处理。当我们想要实现完全异步的请求限流时,我们需要精细地控制每个Future的执行时机。也就是说,我们需要一个开关来控制Future,没错,这个开关就是Promise。每个Promise实例都会有一个唯一的Future与之关联:valp=Promise[Int]()valf=p.futurefor(v<-f){println(v)}//打印操作将在3秒后执行//3秒后返回3Thread.sleep(3000)p.success(3)跨线程错误处理Java通过异常机制处理错误,但问题是Java代码只能捕获当前线程的异常,不能捕获异常跨线程。在Scala中,我们可以使用Future来捕获任何线程中发生的异常。异步任务可能成功也可能失败,所以我们需要一个既能代表成功也能代表失败的数据类型,这就是Scala中的Try[T]。Try[T]有两个子类型,Success[T]表示成功,Failure[T]表示失败。就像量子物理中薛定谔的猫一样,在异步任务执行之前,你无法预测返回的结果是Success[T]还是Failure[T]。结果只能在异步任务执行后才能确定。valf=Future{/*asynchronoustask*/}//当异步任务执行完成时f.value.getmatch{caseSuccess(v)=>//handlesuccesscaseFailure(t)=>//handlefailurecase}wealsoFuture可以从错误中恢复:valf=Future{/*asynchronoustask*/}for{result<-f.recover{caset=>/*handleerror*/}}yield{//handleresult}DeclarativeprogrammingProvocativeindex:四星Scala鼓励声明式编程,声明式风格编写的代码更具可读性。与传统的过程式编程相比,声明式编程更关注我想做什么,而不是如何去做。比如我们经常需要实现分页操作,每页返回10条数据:valallUsers=List(User("jack"),User("rose"))valpageList=allUsers.sortBy(u=>(u.role,u.name,u.addTime))//按role,name,addTime依次排序.drop(page*10)//跳过上一页data.take(10)//取当前页数据,如果有小于10,返回全部您只需要告诉Scala做什么,例如先按角色排序,如果角色相同,则按名称排序,如果角色和名称相同,则按addTime排序。底层具体排序实现已经封装,开发者无需再实现。面向表达式编程预告指数:四颗星在Scala中,一切都是表达式,包括if、for、while等常见的控制结构都是表达式。表达式与语句的不同之处在于每个表达式都有一个明确的返回值。vali=if(true){1}else{0}//i=1vallist1=List(1,2,3)vallist2=for(i<-list1)yield{i+1}不同的表达式可以组合在一起形成a更大的表达,结合模式匹配会发挥巨大的威力。让我们用一个计算加法的解释器来说明。一个整数加法解释器我们首先定义了基本的表达式类型:abstractclassExprcaseclassNumber(num:Int)extendsExprcaseclassPlusExpr(left:Expr,right:Expr)extendsExpr定义了上面两种表达式类型,Number代表一个整数表达式,PlusExpr代表一个Additive表达式。下面我们实现基于模式匹配的表达式求值:defevalExpr(expr:Expr):Int={exprmatch{caseNumber(n)=>ncasePlusExpr(left,right)=>evalExpr(left)+evalExpr(right)}}让我们尝试计算更大的表达式:evalExpr(PlusExpr(PlusExpr(Number(1),Number(2)),PlusExpr(Number(3),Number(4))))//10个隐式参数和隐式转换激发指数:五-star隐式参数如果每次执行异步任务都需要显式传入线程池参数,是不是觉得很烦?Scala通过隐式参数为您解除了这个麻烦。例如,Future在创建异步任务时,会声明一个ExecutionContext类型的隐式参数。编译器会自动在当前范围内搜索合适的ExecutionContext。如果找不到就会报编译错误:implicitvalec:ExecutionContext=???valf=Future{/*asynchronoustask*/}(ec)隐式转换隐式转换比隐式参数使用起来更灵活。如果Scala在编译过程中发现错误,它会先将隐式转换规则应用于错误代码,然后再报告错误。如果在应用规则后可以编译,则意味着隐式转换已成功完成。实现不同库之间的无缝对接当传入的参数类型与目标类型不匹配时,编译器会尝试隐式转换。使用这个功能,我们将现有的数据类型无缝连接到第三方库。比如我们想在Scala项目中使用MongoDB官方的Java驱动进行数据库查询操作,但是查询接口接受的参数类型是BsonDocument。由于使用BsonDocument构建查询比较笨拙,所以我们希望使用Scala的JSON库构建一个查询对象,然后直接传递给官方驱动的查询接口,而不需要改动官方驱动的任何代码。使用隐式转换可以很容易地实现这个功能:implicitdeftoBson(json:JsObject):BsonDocument=...valjson:JsObject=Json.obj("_id"->"0")jCollection.find(json)//编译器会自动调用toBson(json)使用隐式转换,我们可以在不更改第三方库代码的情况下与它比较我们的数据类型无缝。例如,通过实现隐式转换,我们将Scala的JsObject类型无缝连接到MongoDB官方Java驱动的查询接口。看来官方的MongoDB驱动确实提供了这个接口。同时,我们也可以将三方库中的数据类型无缝集成到现有的接口中,只需要实现一个隐式转换的方法即可。扩展现有类的功能比如我们定义一个美元货币类型Dollar:classDollar(value:Double){def+(that:Dollar):Dollar=...def+(that:Int):Dollar=...}所以我们可以做这样的事情:valhalfDollar=newDollar(0.5)halfDollar+halfDollar//1dollarhalfDollar+0.5//1dollar但是我们不能做像0.5+halfDollar这样的事情,因为我们在Double类型上找不到合适的+方法。在Scala中,为了实现上述操作,我们只需要实现一个简单的隐式转换:implicitdefdoubleToDollar(d:Double)=newDollar(d)0.5+halfDollar//相当于doubleToDollar(0.5)+halfDollar更好的运行时性能在日常开发中,我们通常需要将值对象转换成Json格式,以方便数据传输。Java中通常的做法是使用反射,但是我们知道使用反射是要付出代价的,我们要承担运行时的性能开销。另一方面,Scala可以在编译时为值对象生成隐式Json编解码器。这些编解码只是普通的函数调用,不涉及任何反射操作,大大提高了系统的运行性能。.总结如果你坚持读到这里,我会感到很欣慰。很有可能是Scala的一些特性吸引了你。但Scala的魅力远不止于此,上面列出的只是一些最有可能引起您注意的特性。如果你愿意打开Scala的大门,你会看到一个完全不同的编程世界。