Scala 系列(十二)—— 类型参数

一、泛型

Scala 支持类型参数化,使得我们能够编写泛型程序。

1.1 泛型类

Java 中使用 <> 符号来包含定义的类型参数,Scala 则使用 []

class Pair[T, S](val first: T, val second: S) {
  override def toString: String = first + ":" + second
}
object ScalaApp extends App {

  // 使用时候你直接指定参数类型,也可以不指定,由程序自动推断
  val pair01 = new Pair("heibai01", 22)
  val pair02 = new Pair[String,Int]("heibai02", 33)

  println(pair01)
  println(pair02)
}

1.2 泛型方法

函数和方法也支持类型参数。

object Utils {
  def getHalf[T](a: Array[T]): Int = a.length / 2
}

二、类型限定

2.1 类型上界限定

Scala 和 Java 一样,对于对象之间进行大小比较,要求被比较的对象实现 java.lang.Comparable 接口。所以如果想对泛型进行比较,需要限定类型上界为 java.lang.Comparable,语法为 S <: T,代表类型 S 是类型 T 的子类或其本身。示例如下:

// 使用 <: 符号,限定 T 必须是 Comparable[T]的子类型
class Pair[T <: Comparable[T]](val first: T, val second: T) {
  // 返回较小的值
  def smaller: T = if (first.compareTo(second) < 0) first else second
}
// 测试代码
val pair = new Pair("abc", "abcd")
println(pair.smaller) // 输出 abc

扩展:如果你想要在 Java 中实现类型变量限定,需要使用关键字 extends 来实现,等价的 Java 代码如下:

public class Pair<T extends Comparable<T>> {
private T first;
private T second;
Pair(T first, T second) {
  this.first = first;
  this.second = second;
}
public T smaller() {
  return first.compareTo(second) < 0 ? first : second;
}
}

2.2 视图界定

在上面的例子中,如果你使用 Int 类型或者 Double 等类型进行测试,点击运行后,你会发现程序根本无法通过编译:

val pair1 = new Pair(10, 12)
val pair2 = new Pair(10.0, 12.0)

之所以出现这样的问题,是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下: 

// 除了 compareTo 方法外,还提供了额外的关系符方法
trait Ordered[A] extends Any with java.lang.Comparable[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)
}

之所以在日常的编程中之所以你能够执行 3>2 这样的判断操作,是因为程序执行了定义在 Predef 中的隐式转换方法 intWrapper(x: Int),将 Int 类型转换为 RichInt 类型,而 RichInt 间接混入了 Ordered 特质,所以能够进行比较。 

// Predef.scala
@inline implicit def intWrapper(x: Int)   = new runtime.RichInt(x)

https://github.com/heibaiying

要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为 T <% U,代表 T 能够通过隐式转换转为 U,即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下:

// 视图界定符号 <%
class Pair[T <% Comparable[T]](val first: T, val second: T) {
  // 返回较小的值
  def smaller: T = if (first.compareTo(second) < 0) first else second
}

注:由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以如下的视图界定和上面是等效的:

// 隐式转换为 Ordered[T]
class Pair[T <% Ordered[T]](val first: T, val second: T) {
def smaller: T = if (first.compareTo(second) < 0) first else second
}

2.3 类型约束

如果你用的 Scala 是 2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束 (type constraint) 来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下:

 // 1.使用隐式参数隐式转换为 Comparable[T]
class Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T]) 
  def smaller: T = if (first.compareTo(second) < 0) first else second
}

// 2.由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以也可以隐式转换为 Ordered[T]
class Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) {
  def smaller: T = if (first.compareTo(second) < 0) first else second
}

当然,隐式参数转换也可以运用在具体的方法上: 

object PairUtils{
  def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b
}

2.4 上下文界定

上下文界定的形式为 T:M,其中 M 是一个泛型,它要求必须存在一个类型为 M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写:

class Pair[T](val first: T, val second: T) {
  // 请注意 这个地方用的是 Ordering[T],而上面视图界定和类型约束,用的是 Ordered[T],两者的区别会在后文给出解释
  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second 
}

// 测试
val pair= new Pair(88, 66)
println(pair.smaller)  //输出:66

在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例: 

// 1.定义一个人员类
class Person(val name: String, val age: Int) {
  override def toString: String = name + ":" + age
}

// 2.继承 Ordering[T],实现自定义比较器,按照自己的规则重写比较方法
class PersonOrdering extends Ordering[Person] {
  override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1
}

class Pair[T](val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second
}


object ScalaApp extends App {

  val pair = new Pair(new Person("hei", 88), new Person("bai", 66))
  // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译
  implicit val ImpPersonOrdering = new PersonOrdering
  println(pair.smaller) //输出: bai:66
}

2.5 ClassTag上下文界定

这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:Error: cannot find class tag for element type T, 这是由于 Scala 和 Java 一样,都存在类型擦除,即泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。对于下面的代码,在运行阶段创建 Array 时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。

object ScalaApp extends App {
  def makePair[T](first: T, second: T) = {
    // 创建以一个数组 并赋值
    val r = new Array[T](2); r(0) = first; r(1) = second; r
  }
}

Scala 针对这个问题,提供了 ClassTag 上下文界定,即把泛型的信息存储在 ClassTag 中,这样在运行阶段需要时,只需要从 ClassTag 中进行获取即可。其语法为 T : ClassTag,示例如下: 

import scala.reflect._
object ScalaApp extends App {
  def makePair[T : ClassTag](first: T, second: T) = {
    val r = new Array[T](2); r(0) = first; r(1) = second; r
  }
}

2.6 类型下界限定

2.1 小节介绍了类型上界的限定,Scala 同时也支持下界的限定,语法为:U >: T,即 U 必须是类型 T 的超类或本身。

// 首席执行官
class CEO

// 部门经理
class Manager extends CEO

// 本公司普通员工
class Employee extends Manager

// 其他公司人员
class OtherCompany

object ScalaApp extends App {

  // 限定:只有本公司部门经理以上人员才能获取权限
  def Check[T >: Manager](t: T): T = {
    println("获得审核权限")
    t
  }

  // 错误写法: 省略泛型参数后,以下所有人都能获得权限,显然这是不正确的
  Check(new CEO)
  Check(new Manager)
  Check(new Employee)
  Check(new OtherCompany)


  // 正确写法,传入泛型参数
  Check[CEO](new CEO)
  Check[Manager](new Manager)
  /*
   * 以下两条语句无法通过编译,异常信息为: 
   * do not conform to method Check's type parameter bounds(不符合方法 Check 的类型参数边界)
   * 这种情况就完成了下界限制,即只有本公司经理及以上的人员才能获得审核权限
   */
  Check[Employee](new Employee)
  Check[OtherCompany](new OtherCompany)
}

2.7 多重界定

  • 类型变量可以同时有上界和下界。 写法为 :T > : Lower <: Upper

  • 不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 :

    T < : Comparable[T] with Serializable with Cloneable

  • 你可以有多个上下文界定,写法为 T : Ordering : ClassTag 。

三、Ordering & Ordered

上文中使用到 Ordering 和 Ordered 特质,它们最主要的区别在于分别继承自不同的 Java 接口:Comparable 和 Comparator:

  • Comparable:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较;
  • Comparator:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。

为什么 Java 中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了 Comparable 接口,而你又想对其进行比较,这时候当然你可以修改代码实现 Comparable,但是如果这个类你无法修改 (如源码中的类),这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现,所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。

https://github.com/heibaiying

下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例:

3.1 Comparable

import java.util.Arrays;
// 实现 Comparable 接口
public class Person implements Comparable<Person> {

    private String name;
    private int age;

    Person(String name,int age) {this.name=name;this.age=age;}
    @Override
    public String toString() { return name+":"+age; }

    // 核心的方法是重写比较规则,按照年龄进行排序
    @Override
    public int compareTo(Person person) {
        return this.age - person.age;
    }

    public static void main(String[] args) {
        Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)};
        Arrays.sort(peoples);
        Arrays.stream(peoples).forEach(System.out::println);
    }
}

输出:
bai:55
hei:66
ying:77

3.2 Comparator 

import java.util.Arrays;
import java.util.Comparator;

public class Person {

    private String name;
    private int age;

    Person(String name,int age) {this.name=name;this.age=age;}
    @Override
    public String toString() { return name+":"+age; }

    public static void main(String[] args) {
        Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)};
        // 这里为了直观直接使用匿名内部类,实现 Comparator 接口
        //如果是 Java8 你也可以写成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));
        Arrays.sort(peoples, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.age-o2.age;
            }
        });
        Arrays.stream(peoples).forEach(System.out::println);
    }
}

使用外置比较器还有一个好处,就是你可以随时定义其排序规则: 

// 按照年龄大小排序
Arrays.sort(peoples, Comparator.comparingInt(o -> o.age));
Arrays.stream(peoples).forEach(System.out::println);
// 按照名字长度倒序排列
Arrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length()));
Arrays.stream(peoples).forEach(System.out::println);

3.3 上下文界定的优点

这里再次给出上下文界定中的示例代码作为回顾:

// 1.定义一个人员类
class Person(val name: String, val age: Int) {
  override def toString: String = name + ":" + age
}

// 2.继承 Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器
class PersonOrdering extends Ordering[Person] {
  override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1
}

class Pair[T](val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second
}


object ScalaApp extends App {

  val pair = new Pair(new Person("hei", 88), new Person("bai", 66))
  // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器
  implicit val ImpPersonOrdering = new PersonOrdering
  println(pair.smaller) //输出: bai:66
}

使用上下文界定和 Ordering 带来的好处是:传入 Pair 中的参数不一定需要可比较,只要在比较时传入外置比较器即可。

需要注意的是由于隐式默认值二义性的限制,你不能像上面 Java 代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。

implicit val ImpPersonOrdering = new PersonOrdering
println(pair.smaller) 
implicit val ImpPersonOrdering2 = new PersonOrdering
println(pair.smaller)

四、通配符

在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念,用于限定泛型的范围。不同的是 Java 使用 ? 表示通配符,Scala 使用 _ 表示通配符。

class Ceo(val name: String) {
  override def toString: String = name
}

class Manager(name: String) extends Ceo(name)

class Employee(name: String) extends Manager(name)

class Pair[T](val first: T, val second: T) {
  override def toString: String = "first:" + first + ", second: " + second
}

object ScalaApp extends App {
  // 限定部门经理及以下的人才可以组队
  def makePair(p: Pair[_ <: Manager]): Unit = {println(p)}
  makePair(new Pair(new Employee("heibai"), new Manager("ying")))
}

目前 Scala 中的通配符在某些复杂情况下还不完善,如下面的语句在 Scala 2.12 中并不能通过编译: 

def min[T <: Comparable[_ >: T]](p: Pair[T]) ={}

可以使用以下语法代替: 

type SuperComparable[T] = Comparable[_ >: T]
def min[T <: SuperComparable[T]](p: Pair[T]) = {}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 批量操作是指一次性对多个数据进行操作,可以提高操作效率。在使用 Spark 读写 HBase 时,也可以使用批量操作来提高效率。具体实现方式如下: 1. 批量写入数据 使用 HBase 的 Put 类来创建要写入的数据,然后将 Put 对象添加到一个 List 中,最后使用 HBase 的 Table 类的 put 方法来批量写入数据。示例代码如下: ```scala val conf = HBaseConfiguration.create() val connection = ConnectionFactory.createConnection(conf) val table = connection.getTable(TableName.valueOf("table_name")) val puts = new ListBuffer[Put]() for (i <- 1 to 100) { val put = new Put(Bytes.toBytes(s"row_$i")) put.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("col"), Bytes.toBytes(s"value_$i")) puts += put } table.put(puts.toList.asJava) ``` 2. 批量读取数据 使用 HBase 的 Get 类来创建要读取的数据,然后将 Get 对象添加到一个 List 中,最后使用 HBase 的 Table 类的 get 方法来批量读取数据。示例代码如下: ```scala val conf = HBaseConfiguration.create() val connection = ConnectionFactory.createConnection(conf) val table = connection.getTable(TableName.valueOf("table_name")) val gets = new ListBuffer[Get]() for (i <- 1 to 100) { val get = new Get(Bytes.toBytes(s"row_$i")) get.addColumn(Bytes.toBytes("cf"), Bytes.toBytes("col")) gets += get } val results = table.get(gets.toList.asJava) for (result <- results) { val row = Bytes.toString(result.getRow) val value = Bytes.toString(result.getValue(Bytes.toBytes("cf"), Bytes.toBytes("col"))) println(s"$row: $value") } ``` 以上就是使用 Scala 实现 Spark 读写 HBase 的批量操作的方法。 ### 回答2: 在实际的数据处理中,一次需要对多条数据进行读写操作,如果每次都进行单条的读写逐条操作会使程序效率非常低下。所以spark提供了批量操作API,可以对多条数据进行一次性的读写操作,极大地提高了程序的效率。 批量读操作: 批量读取数据的方式有两种:Get和Scan。 使用Get方式读取多条数据,需要将每条数据对应的Get对象添加到List集合当中,再将List集合转换为RDD对象进行操作。示例代码如下: ```scala val conf = HBaseConfiguration.create() conf.set(TableInputFormat.INPUT_TABLE, tableName) val gets = new util.ArrayList[Get]() gets.add(new Get(Bytes.toBytes("rowkey1"))) gets.add(new Get(Bytes.toBytes("rowkey2"))) gets.add(new Get(Bytes.toBytes("rowkey3"))) conf.set(TableInputFormat.SCAN, convertScanToString(new Scan())) val getRdd = sc.parallelize(gets) val hbaseRdd = getRdd.map((_, null)).hbaseBulkGet(conf, tableName, (result: Result) => { val kv: Array[Byte] = result.getValue(Bytes.toBytes(family), Bytes.toBytes(column)) Bytes.toString(kv) }) println(hbaseRdd.collect.toBuffer) ``` 使用Scan方式读取多条数据,需要将Scan对象作为参数传入,再将RDD对象转换为PairRDD并使用hbaseScan方法进行操作。示例代码如下: ```scala val conf = HBaseConfiguration.create() conf.set(TableInputFormat.INPUT_TABLE, tableName) val scan = new Scan(Bytes.toBytes("rowkey1"), Bytes.toBytes("rowkey3")) conf.set(TableInputFormat.SCAN, convertScanToString(scan)) val hbaseRdd = sc.hbaseScanRDD(conf).map((result: Result) => { val kv: Array[Byte] = result.getValue(Bytes.toBytes(family), Bytes.toBytes(column)) Bytes.toString(kv) }) println(hbaseRdd.collect.toBuffer) ``` 批量写操作: 批量写操作可以使用Put对象集合,将多条数据对应的Put对象添加到集合中,并将集合转换成RDD进行操作即可。示例代码如下: ```scala val conf = HBaseConfiguration.create() conf.set(TableOutputFormat.OUTPUT_TABLE, tableName) val puts = new util.ArrayList[Put]() puts.add(new Put(Bytes.toBytes("rowkey1")).addColumn(Bytes.toBytes(family), Bytes.toBytes(column), Bytes.toBytes("value1"))) puts.add(new Put(Bytes.toBytes("rowkey2")).addColumn(Bytes.toBytes(family), Bytes.toBytes(column), Bytes.toBytes("value2"))) puts.add(new Put(Bytes.toBytes("rowkey3")).addColumn(Bytes.toBytes(family), Bytes.toBytes(column), Bytes.toBytes("value3"))) val putRdd = sc.parallelize(puts) putRdd.hbaseBulkPut(conf, tableName) ``` 总结: 批量操作是Spark访问HBase的常见操作方式,在实际的实现过程中需要注意以下几点: 1、Get和Scan对象在HBase中读取数据的方式不一样,需要注意区分; 2、使用批量读操作可以大大提高程序效率,减少读写操作的时间消耗; 3、使用批量写操作需要合理规划写入的数据,避免出现数据冲突问题,影响程序的运行。 ### 回答3: 本篇文章将继续深入介绍如何使用Scala编码实现Spark读写操作HBase,具体涉及到HBase的批量操作。 一、Batch操作概述 在使用HBase进行数据处理的时候,我们常常需要对一个或多个表进行批量操作,批量操作即是针对 HBase的多行进行插入、删除等操作,以此来实现在HBase操作上的高效处理。HBase提供了很多批量操作API,比如 Put、Get、Delete、Scan,这些API都是可以批量操作的。 在Spark中,我们同样可以使用类似的API对HBase进行批量操作。本文将根据具体需求使用Spark实现HBase的批量操作。 二、批量操作的实现 Spark读写HBase时,使用RDD中的foreachPartition来对每个分区进行处理,在该函数内使用HBase API进行操作。关于批量操作,我们可以在每个分区中开启一个batch操作,将每个操作加入batch后,再提交即可。 例如,我们可以考虑实现一个批量put的功能,将RDD中的数据一批一批写入表中: ``` def insert(tableName: String, rdd: RDD[(String, String)]): Unit = { try{ rdd.foreachPartition({ iter => val conf = HBaseUtils.getHBaseConfiguration() conf.set(TableOutputFormat.OUTPUT_TABLE, tableName) val conn = ConnectionFactory.createConnection(conf) val table = conn.getTable(TableName.valueOf(tableName)) val puts = new java.util.ArrayList[Put]() iter.foreach { case (rowKey:String, value: String) => { // 构造put对象并append val put = new Put(Bytes.toBytes(rowKey)) put.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(columnQualifier), Bytes.toBytes(value)) puts.add(put) if (puts.size() >= batchSize) { // 多条记录组成的put对象,使用put(List<Put>)一次性写入 table.put(puts) puts.clear() } } } // 如果puts还有内容,再写一次 if (puts.size() > 0) { table.put(puts) puts.clear() } table.close() conn.close() }) } catch { case e: Exception => e.printStackTrace() } } ``` 在该方法中,我们使用foreachPartition遍历RDD中的每个分区,然后通过Connection来获取HBase表实例。 之后定义了一个用于存放Put的List,当List的大小大于等于batchSize时,就将这个List中的所有put操作提交给HBase执行。 最后,释放资源,并为大家展示如何调用这个方法: ``` val rdd: RDD[(String, String)] = ... val tableName: String = ... insert(tableName, rdd) ``` 使用这种方式实现批量put,我们可以将一批数据提交到HBase执行,从而提升写入效率。当然,对于其他批量操作也可以应用类似的方式。 三、总结 本文根据实际需求,结合Spark和HBase的特点,实现了一些常用的批量操作,为大家提供了一个快速、高效的HBase操作方案。批量操作的好处是,可以将多条记录一次性操作,请求与写入等待时间都会得到缩短,获得更高的效率。感兴趣的同学可以试试,在实际开发中应该会受益匪浅!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值