特质和行为

著名的科学家和研究人员艾萨克·牛顿爵士(Isaac Newton)爵士被誉为:“如果我进一步了解,那就是站在巨人的肩膀上。” 作为一个狂热的历史学家和政治学家,我可能会对这位伟人的名言稍作修改:“如果我进一步看,那是因为我站在历史的肩膀上。” 这些话反映了历史学家乔治·桑塔亚娜(George Santayana)的另一句话:“那些不记得过去的人将被谴责以重蹈覆辙。” 换句话说,如果我们不能回顾历史并从那些摆在我们面前的人(包括我们自己)所犯的错误中吸取教训,那么改善的机会就很小。

因此,您想知道,这种哲学与Scala有什么关系? 继承,一方面。 考虑一下Java语言创建于20年前(即面向对象的鼎盛时期)的事实。 它旨在模仿当今的主流语言C ++,以一种赤裸裸的尝试将这种语言的开发者吸引到Java平台上。 做出了某些决定,这些决定在当时似乎是显而易见的,而且是必要的,但是回想起来,我们知道其中一些并不像当时的创造者所认为的那样有益。

例如,在20年前,对于Java语言的创建者来说,拒绝C ++风格的私有继承和多重继承都是有意义的。 从那时起,许多Java开发人员都不得不后悔自己的决定。 在本月的Scala指南中,我将回顾Java语言中多重继承和私有继承的历史。 然后,您将看到Scala为重写历史记录所做的工作,从而为我们所有人带来了更大的利益。

C ++和Java语言的继承

历史就是人们决定同意的事件的版本。
- 拿破仑·波拿巴

那些在C ++矿山工作的人会记得,私有继承是从基类吸收行为的一种方式,而无需明确接受IS-A关系。 将基类标记为“私有”可以使派生类从其继承而无需实际成为其中一个。 但是,私有继承本身就是其中从未有过的特征之一。 从基类继承而不能向下转换或向上转换到基类的想法似乎很愚蠢。

另一方面,多重继承通常被认为是面向对象编程的必要元素。 在对车辆的层次结构进行建模时, SeaPlane显然需要继承自Boat (使用startEngine()sail() )和Plane (使用startEngine()fly() )。 SeaPlane既是Boat又是Plane ,不是吗?

无论如何,这就是C ++黄金时代的想法。 快进Java语言时,我们会发现多重继承与私有继承一样有缺陷。 相反,任何Java开发人员都会告诉你, SeaPlane应该从接口继承FloatableFlyable (也可能是接口或基类EnginePowered ,以及)。 从接口继承意味着能够实现类所需的所有方法而不会遇到虚拟多重继承的恐惧(在此,我们试图解决在调用SeaPlanestartEngine()方法时要调用哪个库的startEngine()的问题)。

不幸的是,放弃私有继承和多重继承使我们在代码重用方面付出了巨大的代价。 Java开发人员可能会为摆脱虚拟多重继承而欢欣鼓舞,但这种权衡通常是程序员辛苦且容易出错的工作。

可重用的行为,再次

事件...可以大致分为可能从未发生过的事件和无关紧要的事件。
—威廉·拉尔夫·英格

JavaBeans规范是Java平台的基础,从而产生了我们的Java生态系统所依赖的POJO。 我们都知道Java代码中的属性是由get()/set()对管理的,如清单1所示:

清单1. Person POJO
//This is Java      
public class Person
{
    private String lastName;
    private String firstName;
    private int age;
    
    public Person(String fn, String ln, int a)
    {
        lastName = ln; firstName = fn; age = a;
    }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String v) { firstName = v; }
    public String getLastName() { return lastName; }
    public void setLastName(String v) { lastName = v; }
    public int getAge() { return age; }
    public void setAge(int v) { age = v; }
}

这看起来相当简单,而且并不难做到。 但是,如果您想提供通知支持-以便第三方可以在POJO中注册并在属性更改时接收回调,该怎么办? 根据JavaBeans规范,您将必须实现PropertyChangeListener接口及其单个方法propertyChange() 。 如果要允许POJO的PropertyChangeListener的任何一个对PropertyChangeListener更改进行“投票”,则POJO还需要实现VetoableChangeListener接口,该接口需要实现vetoableChange()方法。

至少,这就是应该的方式。

实际上, PropertyChangeListener接口必须由可能的属性更改通知的接收者实现,并且发送者(在这种情况下为Person类)必须提供采用该接口实例的公共方法以及其名称。侦听器想要侦听的属性。 最终结果是清单2中所示的更复杂的Person

清单2. Person POJO,占2
//This is Java      
public class Person
{
    // rest as before, except that inside each setter we have to do something
    // like:
    // public setFoo(T newValue)
    // {
    //     T oldValue = foo;
    //     foo = newValue;
    //     pcs.firePropertyChange("foo", oldValue, newValue);
    // }
    
    public void addPropertyChangeListener(PropertyChangeListener pcl)
    {
        // keep a reference to pcl
    }
    public void removePropertyChangeListener(PropertyChangeListener pcl)
    {
        // find the reference to pcl and remove it
    }
}

保留对属性更改侦听器的引用意味着Person POJO必须保留某种收集类(例如ArrayList )以包含所有引用。 然后必须实例化,插入POJO或从POJO中删除POJO,并且由于这些动作不是原子动作,因此还必须包含适当的同步保护。

最后,如果属性发生更改,通常必须通过遍历PropertyChangeListener的集合并在每个属性侦听器上调用propertyChange()来通知属性侦听器列表。 该过程包括传入一个新的PropertyChangeEvent描述PropertyChangeEvent类和JavaBeans规范所要求的属性,旧值和新值。

难怪我们这么少的书面POJO支持侦听器通知:这是大量的工作,而且对于创建的每个JavaBean / POJO,都必须手工重复进行。

工作,工作,工作-解决方法在哪里?

有趣的是,如果将C ++对私有继承的支持延续到Java语言中,我们今天就可以使用它来解决JavaBeans规范中的一些难题。 基类可以提供POJO的基本add()remove()方法,集合类和“ firePropertyChanged() ”方法,以将属性更改通知给侦听器。

我们仍然可以使用Java类来做到这一点,但是因为Java缺少私有继承,所以Person类将不得不从Bean的基础类继承,因此可以向上转换为Bean 。 这将阻止Person从任何其他类继承。 多重继承可以使我们免于后一个问题,但也会使我们回到虚拟继承,这是我们绝对希望避免的。

Java语言解决此问题的方法是支持类的惯用语,在本例中为PropertyChangeSupport :在POJO内部实例化其中之一,将必要的公共方法放在POJO本身上,并将每个公共方法调用都放入Support班做脏活。 这是更新的Person POJO以使用PropertyChangeSupport

清单3. Person POJO,占3
//This is Java      
import java.beans.*;

public class Person
{
    private String lastName;
    private String firstName;
    private int age;

    private PropertyChangeSupport propChgSupport =
        new PropertyChangeSupport(this);
    
    public Person(String fn, String ln, int a)
    {
        lastName = ln; firstName = fn; age = a;
    }
    
    public String getFirstName() { return firstName; }
    public void setFirstName(String newValue)
    {
        String old = firstName;
        firstName = newValue;
        propChgSupport.firePropertyChange("firstName", old, newValue);
    }
    
    public String getLastName() { return lastName; }
    public void setLastName(String newValue)
    {
        String old = lastName;
        lastName = newValue;
        propChgSupport.firePropertyChange("lastName", old, newValue);
    }
    
    public int getAge() { return age; }
    public void setAge(int newValue)
    {
        int old = age;
        age = newValue;
        propChgSupport.firePropertyChange("age", old, newValue);
    }

    public void addPropertyChangeListener(PropertyChangeListener pcl)
    {
        propChgSupport.addPropertyChangeListener(pcl);
    }
    public void removePropertyChangeListener(PropertyChangeListener pcl)
    {
        propChgSupport.removePropertyChangeListener(pcl);
    }
}

我不确定您的身份,但是该代码的复杂性几乎使我想再次使用汇编语言。 更糟糕的是,您必须在编写的每个POJO中都重复此确切的代码序列。 清单3中的一半工作在POJO本身中,因此不能重复使用-除非“剪切和粘贴”编程的传统可以重复使用。

现在,让我们看看Scala在更好的解决方法中所提供的功能。

Scala的特征和行为重用

每个人都有义务认真思考自己的性格特质。 他还必须充分规范他们,不要怀疑别人的特质是否更适合他。
—西塞罗

Scala使您能够定义一个新的构造,该构造位于接口和称为trait的类之间。 特质是不寻常的,因为类可以根据需要合并任意数量的类(如接口),但它们也可以包含行为(如类)。 同样,像类和接口一样,特征可以引入新的方法。 但是与任何一种情况不同,只有在特征实际上作为类的一部分并入之后,才检查该行为的定义。 或者换句话说,您可以定义在将其合并到使用特征的类定义中之前不会检查其正确性的方法。

性状听起来可能很复杂,但是一旦您看到它们的实际作用,就更容易理解它们。 首先,这是在Scala中重新定义的Person POJO:

清单4. Scala的Person POJO
//This is Scala
class Person(var firstName:String, var lastName:String, var age:Int)
{
}

您还可以通过在类参数firstNamelastNameage上使用scala.reflect.BeanProperty批注,来确保Scala POJO具有在基于Java POJO的环境中期望的get()/set()方法。 现在,我将这些方法排除在等式之外,以使事情变得简单。

如果Person类希望能够接受PropertyChangeListener ,则可以如清单5所示:

清单5. Scala的带有侦听器的Person POJO
//This is Scala
object PCL
    extends java.beans.PropertyChangeListener
{
    override def propertyChange(pce:java.beans.PropertyChangeEvent):Unit =
    {
        System.out.println("Bean changed its " + pce.getPropertyName() +
            " from " + pce.getOldValue() +
            " to " + pce.getNewValue())
    }
}
object App
{
    def main(args:Array[String]):Unit =
    {
        val p = new Person("Jennifer", "Aloi", 28)

        p.addPropertyChangeListener(PCL)
        
        p.setFirstName("Jenni")
        p.setAge(29)
        
        System.out.println(p)
    }
}

请注意,使用清单5中的object是如何使我能够将静态方法注册为侦听器的-如果未显式创建Singleton类并将其实例化,则无法在Java代码中进行此操作。 这只是Scala从Java开发的历史痛点中学到的理论的更多证据。

Person的下一步是在属性更改时,在每个侦听器上提供addPropertyChangeListener()方法和fire propertyChange()方法调用。 在Scala中,以可重用的方式进行操作就像定义和使用特征一样容易,如清单6所示。我将此特征称为BoundPropertyBean因为在JavaBeans规范中正式将“已通知”属性称为绑定属性 。

清单6.神圣的行为重用,蝙蝠侠!
//This is Scala
trait BoundPropertyBean
{
    import java.beans._

    val pcs = new PropertyChangeSupport(this)
    
    def addPropertyChangeListener(pcl : PropertyChangeListener) =
        pcs.addPropertyChangeListener(pcl)
    
    def removePropertyChangeListener(pcl : PropertyChangeListener) =
        pcs.removePropertyChangeListener(pcl)
    
    def firePropertyChange(name : String, oldVal : _, newVal : _) : Unit =
        pcs.firePropertyChange(new PropertyChangeEvent(this, name, oldVal, newVal))
}

同样,我仍在使用java.beans包中的PropertyChangeSupport类,这不仅是因为它提供了我所需的大约60%的实现细节,而且还因为我的行为与那些JavaBeans / POJO相同,直接使用。 此“ Support ”类的任何其他增强功能也将通过我的特征传播。 区别在于,现在Person POJO无需担心直接使用PropertyChangeSupport ,如清单7所示:

清单7. Scala的Person POJO,占2
//This is Scala
class Person(var firstName:String, var lastName:String, var age:Int)
    extends Object
    with BoundPropertyBean
{
    override def toString = "[Person: firstName=" + firstName +
        " lastName=" + lastName + " age=" + age + "]"
}

编译后,快速浏览一下Person定义就会​​发现它具有公共方法addPropertyChangeListener()removePropertyChangeListener()firePropertyChange() ,就像Java版本的Person一样。 实际上,Scala的Person版本仅通过另一行代码就获得了这些新方法:类声明中的with子句将Person类标记为从特征BoundPropertyBean继承。

不幸的是,我还没有完成。 现在, Person类支持接收,删除和通知侦听器,但是Scala为firstName成员生成的默认方法没有使用它们。 而且,同样不幸的是,在撰写本文时,Scala没有漂亮的注释来自动生成使用PropertyChangeSupport实例的get / set方法,因此我必须自己编写它们,如清单8所示:

清单8. Scala的Person POJO,占3
//This is Scala
class Person(var firstName:String, var lastName:String, var age:Int)
    extends Object
    with BoundPropertyBean
{
    def setFirstName(newvalue:String) =
    {
        val oldvalue = firstName
        firstName = newvalue
        firePropertyChange("firstName", oldvalue, newvalue)
    }

    def setLastName(newvalue:String) =
    {
        val oldvalue = lastName
        lastName = newvalue
        firePropertyChange("lastName", oldvalue, newvalue)
    }

    def setAge(newvalue:Int) =
    {
        val oldvalue = age
        age = newvalue
        firePropertyChange("age", oldvalue, newvalue)
    }

    override def toString = "[Person: firstName=" + firstName +
        " lastName=" + lastName + " age=" + age + "]"
}

有一个好的特质

特质几乎不是功能性概念; 相反,它们是对象编程经过十年的深思熟虑的结果。 实际上,您可能会发现自己具有以下特征,甚至在简单的Scala程序中都没有意识到:

清单9. Begone,犯规main()!
//This is Scala
object App extends Application
{
    val p = new Person("Jennifer", "Aloi", 29)

    p.addPropertyChangeListener(PCL)
    
    p.setFirstName("Jenni")
    p.setAge(30)
    
    System.out.println(p)
}

Application特征定义了与您一直手工定义的main()方法相同的方法。 实际上,它还包含另一个有用的小窍门: 计时器 ,如果将系统属性scala.time传递给Application实现代码,它将对应用程序的执行计时(如清单10所示):

清单10.时间就是一切
$ scala -Dscala.time App
Bean changed its firstName from Jennifer to Jenni
Bean changed its age from 29 to 30
[Person: firstName=Jenni lastName=Aloi age=30]
[total 15ms]

JVM中的特征

任何足够先进的技术都无法与魔术区分开。
—亚瑟·克拉克(Arthur C Clarke)

在这一点上,公平地问这个方法接口构造( 又称特征)的魔术是如何映射到JVM上的。 在清单11中,我们的好朋友javap向我们展示了魔术幕背后发生的事情:

清单11. Person ,破解
$ javap -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
    public Person(java.lang.String, java.lang.String, int);
    public java.lang.String toString();
    public void setAge(int);
    public void setLastName(java.lang.String);
    public void setFirstName(java.lang.String);
    public void age_$eq(int);
    public int age();
    public void lastName_$eq(java.lang.String);
    public java.lang.String lastName();
    public void firstName_$eq(java.lang.String);
    public java.lang.String firstName();
    public int $tag();
    public void firePropertyChange(java.lang.String, java.lang.Object, java.lang
.Object);
    public void removePropertyChangeListener(java.beans.PropertyChangeListener);

    public void addPropertyChangeListener(java.beans.PropertyChangeListener);
    public final void pcs_$eq(java.beans.PropertyChangeSupport);
    public final java.beans.PropertyChangeSupport pcs();
}

注意Person的类声明。 该POJO实现了一个称为BoundPropertyBean的接口,该接口是该特征如何映射到JVM本身的方式: 但是该特征方法的实现又如何呢? 请记住,只要最终结果服从Scala语言的语义,编译器就可以发挥各种技巧。 在这种情况下,它将特性中定义的方法实现和字段声明放入实现特性Person的类中。 使用-private运行javap可以使这一点变得很明显-如果还没有从javap输出的后两行中引用它(请参考trait中定义的pcs val):

清单12. Person裂了开来,连服2
$ javap -private -classpath C:\Prg\scala-2.7.0-final\lib\scala-library.jar;classes Person
Compiled from "Person.scala"
public class Person extends java.lang.Object implements BoundPropertyBean,scala.
ScalaObject{
    private final java.beans.PropertyChangeSupport pcs;
    private int age;
    private java.lang.String lastName;
    private java.lang.String firstName;
    public Person(java.lang.String, java.lang.String, int);
    public java.lang.String toString();
    public void setAge(int);
    public void setLastName(java.lang.String);
    public void setFirstName(java.lang.String);
    public void age_$eq(int);
    public int age();
    public void lastName_$eq(java.lang.String);
    public java.lang.String lastName();
    public void firstName_$eq(java.lang.String);
    public java.lang.String firstName();
    public int $tag();
    public void firePropertyChange(java.lang.String, java.lang.Object, java.lang.Object);
    public void removePropertyChangeListener(java.beans.PropertyChangeListener);

    public void addPropertyChangeListener(java.beans.PropertyChangeListener);
    public final void pcs_$eq(java.beans.PropertyChangeSupport);
    public final java.beans.PropertyChangeSupport pcs();
}

实际上,这种解释还回答了如何将特征方法的执行推迟到用于检查之前的问题。 因为直到该类实现特征后,该特征的方法才真正成为该类的“一部分”,所以编译器可以省去以后检查方法逻辑的某些方面。 这很有用,因为它允许特征调用super()而不必知道实现该特征的类的实际基类是什么。

性状或

BoundPropertyBean ,我在构造PropertyChangeSupport实例时使用特征功能。 它的构造函数想要在其上通知属性的bean,并且在前面定义的特征中,我传递了“ this ”。 因为该特性直到在Person上实现后才真正定义,所以“ this ”将引用Person实例,而不是BoundPropertyBean特性本身。 特征的这一特定方面(定义的延迟解析)是微妙的,但对于这种“后期绑定”而言可能是强大的。

对于Application特征,魔术分为两个部分: Application特性的main()方法为Java应用程序提供了无处不在的入口点,并且还检查-Dscala.time系统属性以查看其是否应该跟踪执行时间。 但是,由于Application是一个特征,因此该方法实际上“显示”在子类( App )上。 要执行此方法,必须创建App单例,这意味着构造App的实例,这意味着“播放”类的主体,从而有效地执行应用程序。 只有在完成之后,该特征的main()才会被调用并显示执行所花费的时间。

有点落后,但是它起作用了,但警告是应用程序无法访问传递给main()任何命令行参数。 它还说明了如何将特征的行为“推迟”到实现类中。

性状和收藏

如果您不属于解决方案,那么您就是沉淀的一部分。
—亨利·J·蒂尔曼

当特性将具体行为与抽象声明结合在一起以为实现者提供便利时,它们特别强大。 例如,考虑经典的Java集合接口/类ListArrayListList接口保证可以按插入时的顺序遍历此集合的内容,或者用更正式的术语来说,“尊重位置语义”。

ArrayListList一种特殊类型,将其内容存储在分配的数组中,而LinkedList使用链表实现。 ArrayList更好地用于随机访问列表的内容,而LinkedList更好地用于从列表末尾的任何位置插入和删除。 无论如何,事实证明这两个类之间惊人的行为数量是相同的,结果,这两个类又继承自一个公共基类AbstractList

如果特性在Java编程中得到支持,则对于这种棘手的“可重用行为而不必求助于继承通用基类”这类问题,它们将是一个非常优越的构造。 特质可以充当一种C ++“私有继承”机制,从而避免了新List子类型应直接实现List (并可能忘记实现RandomAccess接口)还是扩展基类AbstractList的潜在困惑。 尽管不要与Ruby mixins(或Scala mixin,我将在以后的文章中进行讨论)相混淆,但在C ++中有时将其称为“ mixin”。

在Scala文档集中,经典示例是Ordered特质,它定义了带有滑稽名称的方法以提供比较(并因此具有排序)功能,如清单13所示:

清单13.订单,订单
//This is Scala
trait Ordered[A] {
  def compare(that: A): Int
  
  def <  (that: A): Boolean = (this compare that) <  0
  def >  (that: A): Boolean = (this compare that) >  0
  def <= (that: A): Boolean = (this compare that) <= 0
  def >= (that: A): Boolean = (this compare that) >= 0
  def compareTo(that: A): Int = compare(that)
}

这里, Ordered的性状(有一个参数化的类型, 一拉的Java 5个泛型)定义了一个抽象方法, compare ,其预计将采取的A作为参数,并需要比1至任一较少返回,如果这是“小于”的是,如果大于等于1,则大于1;如果等于,则大于0。 然后,它继续根据compare()方法以及java.util.Comparable接口也使用的更熟悉的compareTo()方法来定义关系运算符( <>等)。

Scala和Java兼容性

一张图片胜过千言万语。 一个接口值一千张图片。
—本·施耐德曼

实际上,伪实现继承不是Scala中特质的最常见或最强大的用途。 取而代之的是,特征在Scala中是Java接口的基本替代品。 希望调用Scala的Java程序员也应该熟悉特质作为使用Scala的机制。

到目前为止,正如我在整个系列中所指出的那样,已编译的Scala代码并不总是为Java语言提供高保真度。 回想一下,例如,Scala的“带有有趣名称的方法”(例如“ + ”或“ \ ”)通常使用无法在Java语言语法中直接使用的字符进行编码(其中“ $ ”是最大的担心)。 因此,创建“ Java可调用”接口可以简化对Scala代码的调用。

这个特定的示例有些人为的,使用的Scala主义实际上并不需要特质将提供的间接层(假设我没有使用“带有有趣名称的方法”),但请允许我:是这里的关键。 在清单14中,我想要一个传统的Java风格的工厂来生成Student实例,例如您在各种Java对象模型中经常看到的那样。 首先,我需要一个与Java兼容的Student接口:

清单14. I,学生
//This is Scala
trait Student
{
    def getFirstName : String;
    def getLastName : String;
    def setFirstName(fn : String) : Unit;
    def setLastName(fn : String) : Unit;
    
    def teach(subject : String)
}

编译后,这变成了POJI:Plain Old Java Interface,从快速浏览一下javap可以看出:

清单15.这是一台POJI!
$ javap Student
Compiled from "Student.scala"
public interface Student extends scala.ScalaObject{
    public abstract void setLastName(java.lang.String);
    public abstract void setFirstName(java.lang.String);
    public abstract java.lang.String getLastName();
    public abstract java.lang.String getFirstName();
    public abstract void teach(java.lang.String);
}

接下来,我需要一个类来作为工厂本身。 通常,在Java代码中,这将是类的静态方法(称为“ StudentFactory ”之类的东西),但是请记住,Scala没有静态方法之类的东西。 相反,Scala具有对象,它们是带有实例方法的单例对象。 我认为这正是我在这里寻找的内容,因此我创建了StudentFactory对象,并将Factory方法放在此处:

清单16.我让学生们
//This is Scala
object StudentFactory
{
    class StudentImpl(var first:String, var last:String, var subject:String)
        extends Student
    {
        def getFirstName : String = first
        def setFirstName(fn: String) : Unit = first = fn
        def getLastName : String = last
        def setLastName(ln: String) : Unit = last = ln
        
        def teach(subject : String) =
            System.out.println("I know " + subject)
    }

    def getStudent(firstName: String, lastName: String) : Student =
    {
        new StudentImpl(firstName, lastName, "Scala")
    }
}

嵌套类StudentImplStudent trait的实现,因此提供了它所需要的get()/set()方法对。 请记住,尽管特征可以具有行为,但实际上它是根据JVM作为接口建模的,这意味着尝试实例化特征会导致错误,声称Student是抽象的。

当然,这个琐碎的小样本的重要收益是编写一个Java应用程序,该应用程序可以利用这些由Scala创建的新对象:

清单17. Neo学生
//This is Java
public class App
{
    public static void main(String[] args)
    {
        Student s = StudentFactory.getStudent("Neo", "Anderson");
        s.teach("Kung fu");
    }
}

运行此命令,您将看到“我知道功夫”。 (我知道,对于廉价的电影参考而言,这是一个漫长的设置。)

结论

人们不喜欢思考。 如果有人认为,则必须得出结论。 结论并不总是令人满意的。
—海伦·凯勒

性状提供分类和定义Scala中的一个强有力的机制,既定义客户端接口使用, 一拉传统的Java接口,并以此为基础性状中定义的其他行为的行为传承的机制。 也许我们需要的是一个新的继承短语IN-TERMS-OF ,用于描述特征和实现类之间的关系。

使用特质的方式比我在本文中描述的更多,但是本系列的部分目标是提供有关该语言的足够信息,以使您能够在家中进行进一步的实验。 下载Scala实现,进行实验,并查看Scala可以插入当前Java系统的位置。 而且,一如既往,如果您发现Scala有用,对本文有评论,或者您( 叹气 )在代码或散文中发现错误, 给我留言并告知我。

功能迷们要等到下一次。


翻译自: https://www.ibm.com/developerworks/java/library/j-scala04298/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值