first Codec
**public class Friend {
public static void main(String[] args){
System.out.println("BigData加QQ群:947967114");
}
}**
1、布局类库
本章我们的学习目的是构建和渲染二维布局元素的类库。每个元素表示用文本填充的长方形。首先需要提供一个elem的工厂方法。可以用下面这个标签的工厂方法创建一个包含字符串的布局元素:
elem(s:String):Element
我们用一个名为Element的类型对元素建模,可以对一个元素调用above或者beside,传入另一个元素,来把两个元素组合起来。
val columns1=elem(“hello”) above elem(“")
val columns2=elem("”) above elem(“world”)
column1 beside columns2
最终的打印效果
hello *****
***** world
这样一个布局系统,对象可以通过组合操作符(above或beside)的帮助由简单的部件构建。要达到这个目的,首先要构建Element对象,在对象内定义elem工厂方法,并且还要定义above和beside方法。
2、抽象类
首先我们需要定一个Element类型,用来表示元素。由于元素是字符组成的二维矩阵,我们就使用contents来表示布局元素的内容,每个字符串代表一行。contents返回的类型将会是Array[String]:示例如下:
abstract class Element{
def contents:Array[String]
}
类中contents被声明为没有实现的方法,也就是说这个方法是Element的抽象成员。一个包含抽象成员的类本身也要声明为抽象的,实现的方式是在class前面加上abstract修饰符。
abstract class Element
由于是抽象的所以不能直接实例化。new Element是不允许的。如果想使用Element需要创建其子类,这些子类可以被实例化,在子类中填充了抽象的contents方法的实现。
我们会提出一个问题, def contents:Array[String]并没有使用关键字abstract修饰啊,为什么说他是抽象的方法呢??这是scala的特点,和java不同,在scala里一个方法没有实现即没有使用等号并且在等号后面写上方法体,那么这个方法就是抽象的。另一种说法是声明和定义,Element中contents被声明但是没有定义,那么就是抽象的。
3、定义无参的方法
接下来我们给Element添加获取高度和宽度的方法,分别是height和width。height返回contents中的行数,width返回第一行的长度,如果height为0那么width自然为0.
abstract class Element{
def contents:Array[String]
def height:Int=contents.length
def width:Int=if(height0) 0 else contents(0).length
}
我们可以看到Element的每个元素都没有参数,连空参数都没有写。
如果是空参数应该写成:
def height():Int=contents.length
def width():Int=if(height0) 0 else contents(0).length
无参的方法以后将经常看到,一般scala建议对于没有参数且只是通过对象字段的方式访问状态的情况下,尽量使用无参的方法。这样的好处是使用者不会受到某个属性是用字段还是方法实现的影响。
举例来说,以上方法由于是无参方法,所以这些方法完全可以换成字段定义
abstract class Element{
def contents:Array[String]
val height:Int=contents.length
val width:Int=if(height==0) 0 else contents(0).length
}
对于使用方来说,两种方式完全一样,唯一一点区别可能是字段的访问要比方法调用快些。因为字段初始化时就被预先计算好,而方法每次调用时都重新计算。另一方面字段需要Element为其分配额外的内存空间。所以两种方式都有利弊。
对于无参和空括号方法的处理,scala是非常灵活的。可以使用空括号来重写无参方法,反过来亦然。可以在调用空括号方法时省去空括号。例如
scala> Array(1,2,3).toString
res2: String = [I@5a772895
scala> “abc”.length
res3: Int = 3
从原理上来说scala可以省去所有空参方法的括号。
4、扩展类
我们已经说过因为Element是抽象的所以不能使用关键字new出来。那么我们仍然像创建出来怎么办呢?我们需要创建一个扩展自Element的子类,并实现contents的抽象方法。
示例如下:
class ArrayElement(conts:Array[String]) extends Element{
def contents:Array[String]=conts
}
ArrayElement扩展了Element,同样也是使用的关键字extends,extends有两个作用:使得ArrayElement类从Element类继承所有的非私有的成员,并且让ArrayElement成为Element的子类型。ArrayElement是Element的子类,Element就是ArrayElement的超类。如果没有extends那么scala就会默认你继承的是scala.AnyRef,而java是java.lang.Object.
继承是超类的所有成员也是子类的成员,但是两种情况除外,一种是超类的私有成员不会被子类继承,二是如果子类实现了相同名称和参数的方法,那么该成员也不会被继承。这种方式就是重写。如果子类的成员是具体的而超类的是抽象的我们就说这个具体的成员实现了抽象的成员。
在本例中ArrayElement就重写了Element的contents方法,并且从Element继承了width和height两个方法。我们可以定义ArrayElement进行测试:
scala> val aElement=new ArrayElement(Array(“helli”,“world”))
aElement: ArrayElement = ArrayElement@38eb2c50
定义了aElement给了一个Array(“helli”,“world”)值,由于重写了contents,contents作用直接获取Array的值。获取到之后就传递给了height和width方法进行计算。
scala> aElement.height
res5: Int = 2
scala> aElement.width
res6: Int = 5
子类型的意思是子类的值可以被用来在任何需要超类的值的场合。例如:
scala> val e:Element=new ArrayElement(Array(“hello”))
e: Element = ArrayElement@730f9695
变量饿的类型是Element,但是等号后的事实告诉我们它的初始值是ArrayElement类型,因为ArrayElement扩展自Element,所以ArrayElement和Element是兼容的,这种是被允许的。
5、重写方法和字段
在scala中字段和方法是属于同一个命名空间的,这样我们就可以使用字段重写无参的方法。以上两个程序示例,我们可以把ArrayElement的contents从方法改成字段,这并不修改Element类中contents的定义:
class ArrayElement(conts:Array[String]) extends Element{
val contents:Array[String]=conts
}
这里contents字段在ArrayElement中很好的实现了Element的contents方法。因为方法和字段是同一个命名空间所以在scala的一个类中不允许存在相同名称的方法和字段。但java是允许的
Class CompilesFine{
private int f=0;
public int f(){
return 1;
}
}
scala则不能
class WontCompile{
private var f=0
def f=1
//method f is defined twice conflicting symbols both originated in file
}
scala中只有两个命名空间,而java终有四个。
java命名空间:字段、方法、类型、包
scala命名空间:值(字段、方法、包、单例对象)
:类型(类和特质)
6、定义参数化字段
我们看ArrayElement
class ArrayElement(conts:Array[String]) extends Element{
def contents:Array[String]=conts
}
他有一个参数conts,这个字段的唯一目的就是被放在contents上。这里有两个命名contents和conts和容易造成混淆,所以我们完全可以定义成参数化字段,所以我们的ArrayElement直接可以写成:
class ArrayElement
(val contents:Array[String]) extends Element
这样我们就间接实现了def contents:Array[String]=conts获得字段的功能。这是一种同时定义参数和同名字段的简写方式。具体说ArrayElement具备contents字段,该字段可以被外部访问。也可以用var替换val这样contents就可以被重新赋值。还可以添加修饰符,private、public等。
如下示例:
class Cat{
val dangerous=false
}
class Tiger(override val dangerous:Boolean,
private var age:Int
)extends Cat
在tiger的定义中重写了dangerous和定义了私有的age成员。如果不使用这种方式那么应该写成:
class tiger1(param1:Boolean,param2:Int) extends Cat{
override val dangerous=param1
private var age=param2
}
这种方式增加了两个命名,而且还增加了一些代码的工作量。
7、调用超类的构造方法
面向对象的编程让我们很容易用心的数据变种来扩展一个已有的系统,只要添加子类即可。
举例,我们再创建一个LineElement类来扩展ArrayElement。
class LineElement(s:String) extends ArrayElement(Array(s)){
override def width=s.length()
override def height=5
}
由于LineElement是扩展自ArrayElement,而ArrayElement的构造方法是接收参数Array[String],LineElement想要调用ArrayElement的构造方法,只需要把自己的参数传递给ArrayElement即可,也就是把自己的参数放在超类的圆括号内。
如果我们不适用以上方式来定义,但是也要使用继承我们的代码要如下实现:
class LineElement(override val contents:Array[String]) extends ArrayElement(contents:Array[String]){
override def width=contents.length
override def height=5
}
LineElement(override val contents:Array[String])要把ArrayElement(contents:Array[String])的参数放进去作为参数并且用override关键字标明是重新。
8、多态和动态绑定
以上例子程序中,。Element的变量看了一指向一个类型为ArrayElement的对象,这个现象叫做多态,意识是多种形态或多种形式。
到现在为止我们看到了Element的两种形式,ArrayElement和ListElement。还可以通过定义新的Element的子类来创建更多的Element。例如创建一个有指定高度和宽度的Element的子类UniformElement。
class UniformElement(ch:Char,
override val width:Int,
override val height:Int) extends Element{
private val line=ch.toString*width
def contents =Array.fill(height)(line)
}
Element有了这些形态,我们就可以进行如下方式的定义变量并且赋值。
val e:Element=new ArrayElement(Array(“hello”,“world”))
val ae:ArrayElement=new LineElement(“Hello”)
val e2:Element=ae
val e3:Element=new UniformElement(‘a’,2,4)
当我们在编译器如下定义时:
scala> val e:Element=new ArrayElement(Array(“hello”))
e: Element = ArrayElement@730f9695
我们会发现等号后边的类型是等号左边类型的子类,还有另一面的含义,对变量和表达式的调用都是动态绑定的。就是说实际被调用的方法实现是在运行时基于对象来决定。而不是变量或表达式的类型决定,为了展示此功能,我们可以把Element中所有的成员都去掉,并向Element中添加名为demo的方法。
我们的改完之后并且加了测试单例test最终代码如下
abstract class Element{
def demo()={
println(“Elemnt 被定义了”)
}
}
class ArrayElement extends Element{
override def demo(){
println(“ArrayElemnt 被定义了”)
}
}
class LineElement extends ArrayElement{
override def demo(){
println(“LineElement 被定义了”)
}
}
class UniformElement extends Element
object test{
def invokeDemo(e:Element){
e.demo()
}
def main(args: Array[String]): Unit = {
invokeDemo(new ArrayElement)
invokeDemo(new LineElement)
invokeDemo(new UniformElement)
}
}
当我们运行我们发现是如下结果:
ArrayElemnt 被定义了
LineElement 被定义了
Elemnt 被定义了
这样的结果就标明,demo方法是在被调用的时候根据调用的类,检查有没有重新实现,如果实现了调用重写的方式所以invokeDemo(new ArrayElement)
和invokeDemo(new LineElement)的输出是ArrayElemnt 被定义了和LineElement 被定义了,而invokeDemo(new UniformElement)没有重写,所以调用了Element的demo方法。
9、声明final
在设计程序继承关系时,需要确保某个成员不能被继承,和java一样我们只要加上final修饰符来实现。
class ArrayElement extends Element{
final override def demo(){
println(“ArrayElemnt 被定义了”)
}
}
这是
class LineElement extends ArrayElement{
override def demo(){
println(“LineElement 被定义了”)
}
}
就会报错:overriding method demo in class ArrayElement of type ()Unit; method demo cannot override final member
甚至我们可以确保某个类没有子类,可以在声明的时候前面加上final。
final class ArrayElement extends Element{
final override def demo(){
println(“ArrayElemnt 被定义了”)
}
}
当我们尝试继承ArrayElement的时候会出现:illegal inheritance from final class ArrayElement这样的错误。
10、组合和继承
组合和继承是两种用其他已有的类来定义新类的方式,如果主要想代码复用,优先选择组合。继承会受到脆弱基类问题的干扰,会在修改超类时不小心破坏了子类的代码。
继承关系:看建模的关系是不是一个is-a的关系,例如ArrayElement是一个Element。另一个问题使用方是否会把子类当作超类来使用。上面的程序为例,我们把ArrayElement当作了Element来使用:
val e:Element=new ArrayElement(Array(“hello”))
针对上面的LineElement我们是不是把他当作ArrayElement来使用呢?事实是我们让LineElement来继承ArrayElement的主要目的是复用contents方法。基于此原因更好的方式是只要继承Element即可。
class LineElement(s:String) extends Element{
val contents =Array(s)
override def width=s.length()
override def height=5
}
这样我们就定义Element的三个子类ArrayElement、LineElement、UniformElement。现在的LineElement就有了一个跟Array的组合关系,因为Element已经定义了val contents:Array[String],我们定义了一个字符串,指向了字符串数组的引用。简单来说继承,图例说明两者关系:
11、实现above、beside、toString
创建above方法,将某个元素放在另一个元素上面,最开始的above方法只是实现两个元素拼接的功能:
def above(that:Element)={
new ArrayElement(this.contents++ that.contents)
}
使用这个++操作把两个数组拼接起来。事实上这样的代码不够用,因为不允许把宽度不同的元素累加在一起,这个问题先不做处理,只是记住每次传入相同长度的元素给above。后续我们会做修改。
紧接着我们实现beside。把两个元素并排放在一起,需要创建一个新的元素,新元素中每一行都是两个元素的对应行拼接起来的。我们也设计一个把两个相同高度的元素放在一起的beside方法
def beside(that:Element)={
val contents=new ArrayString
for(i<- 0 until this.contents.length)
contents(i)=this.contents(i)+that.contents(i)
new ArrayElement(contents)
}
beside的contents方法被重新分配一个数组,用this.contents和that.contents把对应的数组元素拼接填充在数组里。最后产生一个新的contents的ArrayElement。
针对beside我们可以用另一种方式来实现:
def beside(that:Element):Element={
new ArrayElement(
for (
(line1,line2)<-this.contents zip that.contents
)yield line1+ line2
)
这种方式看起来更简单,是使用了Array的拉链的方法。(line1,line2)<-this.contents zip that.contents意思是来的任何两个元素都进行元素的拉链,最后返回line1+ line2。
我们在定义一种现实元素的方法,只需要通过定义返回格式化好的字符串的toString方法来完成。
override def toString=contents mkString “\n”
toString用到了mkString ,这个方法对所有的序列都适用,包括数组。contents mkString “\n” 这个表达式把contents数组格式化成字符串,每个数组独占一行。
到此为止我们的Element类是这样的:
abstract class Element{
def contents:Array[String]
def height:Int=contents.length
def width:Int=if(height0) 0 else contents(0).length
def above(that:Element)={
new ArrayElement(this.contents++ that.contents)
}
def beside(that:Element):Element={
new ArrayElement(
for (
(line1,line2)<-this.contents zip that.contents
)yield line1+ line2
)
}
override def toString=contents mkString “\n”
}
12、定义工厂方法
现在我们已经有了一个原样类Element,不过我们需要把继承关系藏在工厂对象背后。工厂对象应该包含创建其他对象的方法。使用这些方法来构建对象,而不是直接使用new来构建对象。这种的好处的对象的创建逻辑被集中起来,对象是如何用具体类表示被隐藏,使我们的代码更少的暴露,安全性会好很多。
我们要在哪里放工厂方法呢?在单例对象和是类内?直接的方案是创建一个Element的伴生对象。作为布局工厂对象。这样我们只暴露Element类给使用方,把ArrayElement、LineElement、UniformElement都隐藏起来。
object Element{
def elem(contents:Array[String]):Element={
new ArrayElement(contents)
}
def elem(chr:Char,width:Int,height:Int):Element={
new UniformElement(chr,width,height)
}
def elem(line:String):Element={
new LineElement(line)
}
}
有了这些工厂方法,我们将Element类的实现做一些更改,让他用elem工厂方法,而不是直接用ArrayElement创建。为了调用Element工厂方法时不现实的给出单例对象的限定词,我们需要在源文件中引入Element.elem,在代码的顶部加入import Element.elem,并且我们之前直接用new方法来创建ArrayElement的地方可以直接使用elem方法创建了,现在我们的Element是这样的代码
import Element.elem
abstract class Element{
def contents:Array[String]
def height:Int=contents.length
def width:Int=if(height0) 0 else contents(0).length
def above(that:Element):Element=
elem(this.contents++ that.contents)
def beside(that:Element):Element={
elem(
for (
(line1,line2)<-this.contents zip that.contents
)yield line1+ line2
)
}
override def toString=contents mkString “\n”
}
因为有了这个工厂方法,ArrayElement、LineElement、UniformElement这些子类都可以变成私有的了,因为不需要使用方直接访问了。scala中允许在其他类或者单例对象中定义类和单例对象,因此我们把他们都放在Element的伴生对象中。最后我们的伴生对象是这样的:
object Element{
private class ArrayElement
(val contents:Array[String]) extends Element
private class LineElement(s:String) extends Element{
val contents =Array(s)
override def width=s.length()
override def height=1
}
private class UniformElement(
ch:Char,
override val width:Int,
override val height:Int) extends Element{
private val line=ch.toString*width
def contents =Array.fill(height)(line)
}
def elem(contents:Array[String]):Element={
new ArrayElement(contents)
}
def elem(chr:Char,width:Int,height:Int):Element={
new UniformElement(chr,width,height)
}
def elem(line:String):Element={
new LineElement(line)
}
}
一小段测试代码:
def main(args: Array[String]): Unit = {
val cu=elem(“hello”) above elem(“world”)
val cu2=elem(“nihao”) above elem(“shide”)
print(cu beside cu2)
}
测试结果如下:
13、增宽和增高
到这里Element的雏形已经形成了,已经可以进行拼接和合并操作,但是显然还不够强健。以为不能把不同宽度的元素爹在一起。或把不同高度高度的元素并排放置。例如:
new ArrayElement(Array(“hello”)) above
new ArrayElement(Array(“world”))
是可以放在一起的,但是下面不行
new ArrayElement(Array(“hello scala”)) above
new ArrayElement(Array(“world”))
如果想要达到想要的结果,需要添加两个助手,widen和heighten,用空格来填充行方向和列方向上不足位。above需要借助widen方法,而beside需要借助heighten方法。经过修改我们的程序编程了:
我们的程序编程了这样:
package testSpark
import Element.elem
abstract class Element{
def contents:Array[String]
def height:Int=contents.length
def width:Int=contents(0).length
def above(that:Element):Element={
val this1=this widen that.width
val that1=that widen this.width
elem(this1.contents++that1.contents)
}
def beside(that:Element):Element={
val this1=this heighten that.height
val that1=that heighten this.height
elem(
for ((line1,line2)<-this1.contents zip that1.contents)
yield line1+ line2)
}
def widen(w:Int):Element=
if(w<=width) this
else{
val left=elem(' ',(w-width)/2,height)
val right=elem(' ',w-width-left.width,height)
left beside this beside right
}
def heighten(h:Int):Element={
if(h<=height) this
else{
val top = elem(' ',width,(h-height)/2)
val bot = elem(' ',width,h-height-top.height)
top above this above bot
}
}
override def toString=contents mkString "\n"
}
object Element{
private class ArrayElement
(val contents:Array[String]) extends Element
private class LineElement(s:String) extends Element{
val contents =Array(s)
override def width=s.length()
override def height=1
}
private class UniformElement(
ch:Char,
override val width:Int,
override val height:Int) extends Element{
private val line=ch.toString*width
def contents =Array.fill(height)(line)
}
def elem(contents:Array[String]):Element={
new ArrayElement(contents)
}
def elem(chr:Char,width:Int,height:Int):Element={
new UniformElement(chr,width,height)
}
def elem(line:String):Element={
new LineElement(line)
}
}
为了测试我们程序的效果,我们可以做一个效果图,一个绘制螺旋的程序。
package testSpark
import Element.elem
object Sprial{
val space=elem(" “)
val corner=elem(”+")
def spiral(nEdges:Int,direction:Int):Element={
if (nEdges1)
elem("+")
else{
val sp= spiral(nEdges-1,(direction+3)%4)
def verticalBar = elem(’|’,1,sp.height)
def hoeizontalBar = elem(’-’,sp.width,1)
if(direction0)
(corner beside hoeizontalBar) above (sp beside space)
else if(direction1)
(sp above space) beside (corner above verticalBar)
else if (direction2)
(space beside sp) above(hoeizontalBar beside corner)
else
(verticalBar above corner) beside (space above sp)
}
}
def main(args: Array[String]): Unit = {
val nSides=args(0).toInt
println(spiral(nSides, 0))
}
}