Ruby 和 Java 的基础语法比较

前言

悉尼华助中心太极拳老师为华裔长者新开免费太极班-中国侨网

这篇文章示例代码比较多, Java 程序员可以看到一些 Ruby 相关语法和使用,Ruby 程序员可以看看 Java 的基本语法和使用方法,本文比较长,将近万字左右,预计需要十几分钟,如果有耐心读完文章的话,你将获得和了解:

  • Ruby 语言的基本语法和使用方式
  • Java 语言的基本语法和使用方式
  • 从老司机的角度分析和讲解 Ruby 和 Java 语言语法的特点和区别
  • 它们的各自适合并且擅长的应用场景

网上单独介绍 Ruby ,Java 的文章应该很多,但是对比两种编程语言的基本语法使用的文章应该不多见,写这篇文章的目的主要是对自己近期几个月学习 Ruby 做总结和回顾,我之前最熟悉的编程语言是 Java,我个人认为合格的程序员应该掌握多门语言,多学一门语言没有坏处,在解决问题的时候可以多些思路,在经历最近几个月的横向对比和使用感受,先抛我个人结论,在个人项目或者小型团队,技术能力较强的团队我推荐使用 Ruby, 在团队需要快速扩展和大型项目规划的情况下我推荐 Java,因为得益于 Java 语法的严谨性和安全性,很大程度上可以保证团队水平的下限,Java 较强的工程化规约和代码类型检查,可以保证新手不至于写出破坏性很强的代码,如果把两种语言作为一个简单的比如,最直观的感受就是可以把 Ruby 和 Java 比做金庸小说里的两把武器:

  • Ruby 设计精妙,体积小巧灵活迅捷如风,就像紫薇软剑那般锋芒毕露,使用者可以随心所欲,不必被太多语法和规则限制
  • Java 老成持重,虽然语法和年代较为古板啰嗦,但是却长年占据 TIOBE 编程语言排行榜第一名,真可谓是重剑无锋,大巧不工

在很多人的印象中 Ruby 主要是在初创公司会比较流行,例如早期的 AirbnbGitLab 都是使用 Ruby 作为开发语言,Ruby 是一门很灵活也很优雅的动态语言,解释运行,有兴趣了解的同学可以点开 链接 查看维基百科的词条,Ruby 语法精炼,做相同的事情代码行数通常会比 Java 要短的多,使用 Ruby 写程序的的过程是非常舒服的,因为不必拘泥于那些刻板强制的语法规范,可以让开发者随心所欲的表达自己的想法,不必强制分号结尾,不强制中括号,不强制方法参数长度等语法规范所限制,这种灵活的表达方式设计体现在语言使用的方方面面,并且如果你是用 Mac OS 则系统天生支持 Ruby 开发环境,在 Mac 终端 输入以下命令就可以看到 Ruby 版本号:

ruby -v
# ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]

然后只要在终端里面键入 irb 就可以进入调式模式,不像要运行 Java 程序首先安装 JDK 然后再配置环境变量 JAVA_HOMECLASS_PATH 经过繁琐的配置才勉强可以执行 java 命令执行 class 程序,在 irb 模式下,对于简单的逻辑程序可以先在调式模式将代码写出来验证想法的可行后再加入到代码库中去,使用起来非常的方便,示例如下:

>irb
>2.6.5 :001 > p "hello world"
# => "hello world"

下面简单的写一个 Hello World 程序进行对比,两种编程语言在打印 HelloWorld 程序上的写法,示例代码如下:

// java print
public class Main {
    public static void main(String[] args) {
        System.out.println("hello world!");   // 注意分号结尾
    }
}
# ruby print
p "hello world!"

通过一个简单的 Hello World 程序你就可以发现两者的明显区别:

  • Ruby 的执行是从上到下顺序执行,main 方法则是 Java 程序的唯一入口
  • Ruby 不必用 ; 号结束符,不必使用 {} 声明代码块,函数式方法传参甚至不用使用 () (挺有意思)

经过以上讲解,大家可能会对开始产生一些兴趣,不过这仅仅只是开始,后面主要简单介绍一下 Ruby 常用的对象,条件,循环,方法,运算符,数值,数组,字符串,散列等使用方法,本文不算严格意义的文章,因为示例代码量占了文章的 50% ,而且本文的特点就是会在语法将 Ruby 和 Java 进行对比,不过还是会讲解 Ruby 基本语法为主,本文偏入门级水平,介绍的内容都是平时使用比较的多的场景,暂时不会涉及到例如 Ruby 的 metaprogramming 和 Java 的 反射等较为深入的知识点,可能后续会有单独的文章进行分析,看完文章应该可以用写一些简单的程序用于跑一些简单的脚本应该是够用了,实际上脚本处理程序也正是 Ruby 很擅长的领域

补充:文章对比 Java,Ruby 两种语言在语法上的区别,并不是争论哪种编程语言的好坏优劣,我个人观点是:编程语言本身没有好坏之分,只有在不同场景下做出合适的选择,而且熟悉 Java 的同学也应该明白 Java 的优势从来都不在语言和语法层面,而是在生态,并发编程,虚拟机和稳定这些特性才是 Java 的核心竞争力,在生态上 Spring Framework为代表的高质量轮子覆盖 Java 应用的方方面面,可以说在轮子的多样性上面,暂时没有哪种语言可以跟 Java 抗衡,所以如果简单的用语法概括语言的好坏就非常以偏概全的看法,话不多说,我们进入正题,先列一下文章大纲,入门篇只会简单说一些基本语法:

  • 多重赋值
  • 条件判断
  • 循环
  • 方法
  • 类和模块
  • 运算符
  • 异常处理

多重赋值

每个变量单独赋值的场景大多相同,就不做介绍,在程序开发中,我们经常会把多个变量同时赋值,这样效率会高很多,每种语言对多重赋值的支持都不同,我们先通过一段代码对比 Java,Ruby 语言对于多重赋值的不同写法:

// Java 多重赋值
int a, b, c = 1, 2, 3;      // compile error
int a, long b, short c = 1, 2L, 3;  // complier error
int a = 1, b = 2, c =3;     // compile pass 
# Ruby 中多重赋值非常轻松
a, b, c = 1, 2, 3
# => [1, 2, 3]

# 兼容不同类型
a, s, c = 1, "2", 3
# => [1, "2", 3]
    
# 兼容不同长度
a, b, c = 1, 2
# => [1, 2, nil]
a, b, c, = 1, 2, 3, 4
# => [1, 2, 3]  自动裁掉超出的长度

结合以上案例感觉 Java 对多重赋值不是很友好,很多不合规范的语法在编译期就会被拦截并且报错,简单对比后总结:

  • Java 因为强类型,所以对赋值的比较限制多,例如只能对同类型的变量进行简单的赋值
  • Ruby 中多重赋值比较轻松,不用考虑类型,长度等问题,过长和过短都不会在编译时抛出问题
  • Ruby 在声明类型的时候不需要像 Java 那样声明类型,这也是动态语言的特性,我个人是比较喜欢的

条件判断

Ruby 的条件判断主要有以下三种:

  • if 语句
  • unless 语句
  • case 语句

先看实例和对比代码:

a, b = 10, 20
p "a 比 b 大" if a > b
p "a 比 b 小" if a < b
# => a 比 b 小

# unless 条件就不多做介绍,用法刚好与 if 语句相反,类似java中的 != 运算符,实例代码:
a, b = 10, 20
p "a 和 b 不相等" unless a == b
# => a 和 b 不相等
int a = 10, b = 20;
if (a > b) System.out.println("a 比 b 大");	// 在合作项目中还是推荐使用中括号 {}
if (a < b) System.out.println("a 比 b 小");
//=> a 比 b 小

int a = 10, b = 20;
if (a != b) System.out.println("a 和 b 不相等");
//=> a 比 b 小

还有 case 语句主要用于多条件进行判断,语句用法是 casewhenend 进行组合条件判断,功能跟 Java 中的 switch 相同,还有逻辑运算符 ==, !=, ||, && 都是通用的基本知识,所以就不写详细说明和写示例代码了,不然会显得很啰嗦

总结:
条件判断语句用法非常简单,两种编程语言基本类似语言类似,不过还是有以下区别:

  • Ruby 在关键字选择上多一些,例如 unless 实际上是替代了运算符 !=,也增加了一些可读性
  • if 语法基本相似,但 Java 强制表达式必须使用括号 () ,Ruby则不需要
  • Ruby 使用 ifthenend 语法标记代码块,不同于 Java 使用中括号 {} 标记代码块
  • Ruby 条件判断 if/unless 放在代码后面,程序看上去可以更加紧凑和简洁

循环

Ruby 的循环结构语句比较丰富,相比 Java 只有 for,while 两种循环方式来说,Ruby 中的可用的循环方法有:time,while,each,for,until,loop,不过大多都异曲同工,就不一一介绍了,本章节主要围绕平时常用的几个需求来做一个简单的讲解,对比两种语言的使用区别,具体如下:

  • 如何执行一个固定次数的循环 ?
  • 如何遍历一个数组 ?
  • 如何遍历一个 Hash ?

执行固定次数的循环是 time循环 方法的拿手好戏,用于和语句也很简单,如果不需要下标值,|i| 参数也是可以移除的,示例代码如下

3.time do |i|  # i 也可以省略
   p "第#{i}次打印"
end
# => 第0次打印
# => 第1次打印
# => 第2次打印

在 Java 中想要执行固定长度的循环,不能通过 forEach只能通过古老的 for..i 来实现,具体代码如下:

for (int i = 0; i < 3; i++) {
    System.out.println("第" + i + "次打印");
}
// 第0次打印
// 第1次打印
// 第2次打印

如何遍历一个数组?

在 Ruby 中通常会推荐使用 **each ** 不仅语法简单,而且可以轻松拿到元素值,示例代码如下:

["abc","efg","hmn"].each do |e|
  p "#{e}!" 
end
#=> abc! dfg! hmn!

Java 在 JDK 8 经过 StreamLambda 语法增强后,遍历数组也没有想象中那么古板,示例代码:

Arrays.asList("abc", "dfg","hmn").forEach(e -> 
		System.out.println(e + "!")
);
// abc! dfg! hmn!

不过在平时遍历数组的时候经常会遇到一种需求,不仅想要拿到数组的元素,还需要拿到当前循环的索引值,Ruby 中提供一个特别的 each 方式实现,就是 each_with_index 方法,它会把 [元素, 索引] 传入到 do 代码块的后,具体示例代码:

["abc","def","ghi"].each_with_index do |e, i|
  p "当前元素 #{e} , 以及第 #{i} 次循环"
end
#=> "当前元素 abc , 以及第 0 次循环"
#=> ...

Java 想要实现相同循环效果就不能用基于迭代器的 ForEach 实现了,只能用 for..i 实现,示例代码如下:

List<String> list = Arrays.asList("abc", "deg", "ghi");
for (int i = 0; i < list.size(); i++) {
    String e = list.get(i);
    System.out.println("当前元素" + e + ",以及第 " + i + "次循环");
}
// 当前元素abc,以及第 0次循环
// ..

如何遍历一个 Hash ?

Hash 是 Ruby 的常用的对象,因此循环遍历获取 K,V 也是相当方便的,示例代码:

hash = {name: "apple", age: 15, phone: "15815801580"}
hash.each do |k, v|
	p "key: #{k}, value: #{v}"
end
#=> key: name, value: apple
#=> ...

Java 中最常用的 K-V 结构的 Hash 实现是基于 Map 接口的 HashMap,它是一种非线程安全的哈希表实现,之所以常用是因为它兼顾的效率和时间的平衡,内部是通过数组实现,采用使用链表法处理哈希冲突,JDK 8 后又引入 红黑树 解决哈希冲突过多导致链表过长的问题,这块就先不展开讲了,不然可以讲很久,示例代码展示 Java 如何遍历 Hash:

Map<String, String> hashMap = new HashMap<>();
hashMap.put("name", "apple");
hashMap.put("age", "15");
hashMap.put("phone", "15815801580");

for (Map.Entry<String, String> entry : hashMap.entrySet()) {
		System.out.println("key :" + entry.getKey() + ", value : " + entry.getValue());
}
// key :name, value : apple
// ....

Java 遍历 Hash 的方式还有很多种,我们这里只展示最常用的用法,通过 ForEach 遍历 entrySet() 返回的集合即可。

最后再说一个有意思的循环方法,不过使用场景应该很少,一个没有终止的循环 loop方法,因为没有终止条件,所以必须依赖 break 关键字跳出循环,Java 也可以很轻松实现这种循环效果,只是语法上不同而已,我们可以看看以下实例对比:

// java 使用 while(true) 或者 for(;;) 实现无限循环
while (true) System.out.println("i use java");
# ruby 无限循环
loop do
  p "i use ruby"
end

如果程序进入无限循环就只能通过 CTRL + C 来终止程序运行了
总结:循环上两种语言区别不大,Ruby 虽然循环方式多,但是平时常用的也就 eachfor 会比较多,在循环上的区别,大多只是两种语言在语法上的区别

方法

分类

Ruby 中的方法大致可分为 3 类:

  • 实例方法
  • 类方法
  • 函数式方法

实例方法:Ruby 中的实例方法 Instance method 和 Java 中的普通方法类似,顾名思义就是调用方必须是一个类的实例(对象),需要调用实例方法就必须先通过类构造一个实例对象才能进行调用,具体请看示例代码:

# ruby 中的实例方法
 [1, 2, 3] .clear # 清理数组 =>  []
100.to_s   # int 转 string  => "100"
"100".to_i  # string 转 int => 100
["a", "b", "c"].index("b")  # 查找下标 => result: 1
// java 中的实例方法
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("abc");		// 实例方法 append
stringBuilder.append("efg");

List<String> strList = new ArrayList<>();
strList.add("abc");		// 实例方法 add
strList.add("efg");

类方法:Ruby 的类方法 class method 可以理解为 Java 的静态方法,就是需要类对象作为接收方的方法,指无需构建类的对象即可以直接通过类调用其自身的方法,大多常见于工具类当中,请看示例代码:

// java 中的静态方法
Arrays.asList(T...a)    // 数组转集合
Executors.newCachedThreadPool()  // 创建线程池
# ruby 中的类方法
Hash.new  # 创建散列对象
Time.new  # 创建时间对象

函数方法是指没有接收者的方法,这种类型方法在Java中倒是不存在,参考示例代码,例如上文中函数方法 p

p "hello"
puts "print words"

定义实例方法

Ruby 定义方法非常简单,没有 Java 那么多的格式规范:修饰符:静态声明:返回值:方法名:(参数...),在方法声明的形式上要简单许多,主要通过 def 关键字定义,具体参考示例代码:

// java define method
public static int add(int x, int y) {
   return x * y
}
add(2, 5)
// 10
# ruby define method 
def add(x, y)
    x * y
end	

add(2, 5)
#=> 10

# 带默认值的方法
def add(x=2, y=6)
    x * y
end

# 省略参数使用默认值调用方法
add
#=> 12

# 指定参数的方法
add(2, 5)
#=> 10

在方法的命名规则,两种语言还有如下区别:

  • Ruby 方法名可由英文字母,数字,下划线组成,但不能以数字开头,例如 hello_with_name
  • Java 方法名首字母必须由英文小写开头,英文格式遵循驼峰原则,不允许出现连接符,例如 addPerson

返回值return: 上面的 ruby 方法并没有声明 return 语句也可以拿到返回值,并不代表 ruby 没有 return 关键字,ruby 有一个特点就是如果没有声明 return 语句那么方法最后一个表达式会成为方法的返回值遵循这个约定所以上述的方法就可以省略 return 关键字,所以在日常开发中会较少的使用 return 关键字

定义类方法

前面讲过 Ruby 的类方法实际上就等于 Java 的静态方法,Ruby 中定义类方法的示例代码:

# ruby 定义类方法
class Hello

	# class << self 定义类方法的一种方式
	class << self
		def say_morning
			p "hello good morning"
		end

	#... class << self 块后面还可以定义多个类方法
	end
end

Hello.say_morning  # 类方法
#=> "hello good morning"
  
h = Hello.new
h.say_morning  # 如果用实例对象类方法就会报错! undefined method 'say_morning'

在 Java 中定义静态方法的示例:

public class Hello {

    public static void sayMorning() {
        System.out.println("hello good morning");
    }

    public static void main(String[] args) {
        Hello.sayMorning();
    }
}

#=> "hello good morning"

定义方法也都是很简单的知识点,通过以上程序,我们可以得出:

  • Ruby 使用 class << self 或者 class << 类名 可以将代码块内的方法全部声明为类方法
  • Java 使用 static 修饰符定义静态方法,不能定义块,我想可能因为规范和可读性的原因

Ruby 的特点是特定的功能都可以有N种不同的方式实现,所以定义类方法不但只有 class << self ~ end 还可以使用 def class_name.method_name ~ end 这种形式定义 class_name 是类名,method_name 为方法名,如果是在当前 class 上下文中可以像示例代码用同样的形式 def self.method_name ~ end 定义类方法

方法参数

默认参数 Rudy 的方法默认参数是我个人比较喜欢的特性,Java 程序里方法参数是强类型检查的,就是必须按照参数定义的类型进行传参,JDK 1.5 后 Java 也出了可变参的特性,不过因为实现效果不是很理性,目前在主流 Java 开发规范中还是不被推荐使用的,我们先看一段 Java 定义参数和使用参数的示例代码:

// 方法要求类型,顺序,并且必传
public void show(String name, int age, String address) {
    System.out.println(name + ", " + age + ", " + address);
}

//  new Person().show("胖大海");          // 编译出错,参数不符合要求
//	new Person().show("胖大海", 19);          // 编译出错,参数不符合要求
		new Person().show("胖大海", 19, "北京朝阳区");     // 编译通过输出:胖大海, 19, 北京朝阳区

Ruby 则会灵活一些,具体请看示例代码:

	# 无需声明类型,带默认值的参数可不传
	def show(name, age=18, address="北京市")
		p "#{name}, #{age}, #{address}"
	end

Person.new.show("胖胖")  #=> 输出:胖胖, 18, 北京市

不过对于可变参数两种语言的实现几乎相同,形式上都是对参数进行特殊标记,Java 是通过在参数前面加...标识,ruby 则在参数前面使用 * 号标识,解释器会对这种语法用数组进行转换,两者代码量也差不多,没有什么差别,简单看下示例代码:

    public void names(String ...names) {
        System.out.println("params :" + Arrays.toString(names));
    }

    new Person().names("java", "ruby", "go", "c++"); 
// 输出结果 :params :[java, ruby, go, c++]
	def names(*names)
		p "params: #{names}"
	end

Person.new.names("java", "ruby", "go", "c++")
# 输出结果:params: [\"java\", \"ruby\", \"go\", \"c++\"]

简单通过 2 段代码的对比,我们可以对两种语言的方法参数得出以下结论:

  • Java 方法会严格按照定义,强制要求类型,值必传,否则编译期会报错,并且无法在声明时定义参数的默认值
  • Ruby 方法参数未设定默认值,不传参数,只会在执行期报错,但如果声明时定义参数默认值,则参数可不传
  • Ruby 方法参数无需定义类型,动态语言的类型大多是推断出来的
  • 可变参数两者实现方式相同,Java 通过 类型...names 实现,Ruby 通过 *names 语义实现

方法的基本使用大概就讲到这里,函数方法定义平时使用不多就暂时先不聊,继续了解还可以看看:定义带块的方法,关键字参数等都是一些语法糖,就不详细讲解了,接下来聊聊类和模块

类和模块

Ruby 也是通过 class 关键字定义类,简单的用法参考以下代码:

class Hello
end

h = Hello.new

Java 也是通过 class 定义类,不同的是最外层的类 Java 必须使用 public 进行修饰,具体请看示例代码:

public class Hello {

    public static void main(String[] args) {
        Hello h = new Hello();
    }
}

那么 Ruby 和 Java 在定义类方面有什么区别?

  • Ruby 类只有 initialize 构造函数,Java 可以根据参数不同定义不同的构造函数,Java 构造函数必须于类名相同
  • Ruby 和 Java 在类的命名规则上是一致的,类名必须是首字母大写开头
  • Java 通过 public class 修饰类(内部类通过 class 修饰),Ruby 则通过 class 修饰类
  • Java 类名必须与文件名相同,Ruby 的文件名和类名不要求强制关联

两种编程语言在构造函数上对比的示例代码:

# 带构造函数的 Ruby 类
class Hello
	def initialize(name="Ruby")	# 默认参数
		@name = name
	end

	def say_hello		# 实例方法
		p "hello #{@name} ! how are you?"
	end
end

h = Hello.new("james")
h.say_hello
#=> "hello james ! how are you?"
// 带构造函数的 java 类
public class Hello {

    private String name;

  	// 有参构造函数
    public Hello(String youName) {
        this.name = youName;
    }

    public void sayHello() {
        System.out.println("hello! " +name+ " how are you ?");
    }

    public static void main(String[] args) {
        Hello h = new Hello("jack");
        h.sayHello();
    }
}
#=> "hello! jack how are you ?"

方法聊到这里,下来聊聊方法里的常量

常量对比

如果在 Java 和 Ruby 中定义常量,参考示例代码:

// Java 中定义常量
public class Hello {
		// 常量必须是 static final 修饰,代表不可变
    public static final String VERSION = "1.0";

    public static void main(String[] args) {
      	// Hello.VERSION = "1.5";				// 尝试修改则会在编译期报错
        System.out.println(Hello.VERSION);
    }
}
#=>"1.0"
# Ruby 中定义常量
class PhoneNumber
	BeiJing = "020"
	GuangZhou = "021"
end

p PhoneNumber::BeiJing		#=> "020"
p PhoneNumber::GuangZhou		#=> "021"

Phonenumber::BeijING = "303" 	#=> Ruby 可以修改常量,不会报错,但会提示警告

p PhoneNumber.Beijing  #=> ERROR undefined method !

在定义常量上的区别:

  • 命名规则:Ruby 要求常量首字母大写,可用驼峰也可全大写,Java 则要求常量全部大写,并且必须是 final static 修饰(Java 里的 final 代表不可变,可以声明类,方法和变量)
  • 调用方式:Ruby 必须使用 :: 通过类名进行外部访问常量,java 把常量只是当成普通的局部变量,使用连接符 . 直接访问即可
  • 修改变量:Java 不允许修改常量,任何修改的动作会让编译器报错 Connot assign a value to final variable 并且无法通过编译,Ruby 则不同,允许修改常量,但解释器会提示警告信息:warning: already initialized constant

访问级别

Ruby 和 Java 在方法访问级别上没有什么很大不同,只是 Ruby 没有包(Package)的概念,所有自然也就没有 Java 里面的包访问权限,细节上但是还有些许区别,Ruby 的三种访问级别的定义方法,具体用法直接看示例代码:

	# 定义方法时声明访问权限
	private def call_v1
		p "call_v1 is private"
	end

	# 定义方法后,再设定访问权限
	def call_v2
		p "call_v2 is private"
	end

	private :call_v2		# 设定 call_v2 为 private

	# 对代码块设定, 以下的方法定义为 private 
	private
	def call_v3
		p "call_v3 is private"
	end

	def call_v3
		p "call_v4 is private"
	end

Java 设定方法访问级别的方式则比较单一,只能在定义时声明:

    private String priv() {
        return "priv is private";
    }

综上所述,两种语言在访问级别的差异和总结:

  • Java 方法默认修饰符是 包访问权限
  • Ruby 方法默认访问级别是 public(initialize 例外)
  • Java 方法只能在定义的时候使用关键字设定访问级别
  • Ruby 常用的则有三种方式可以设定方法的访问级别,非常灵活

继承

Ruby 和 Java 的所有类都是基于 Object 的子类,Ruby 则还有更加轻量级的 BasicObject原始类,这里先不详细描述,继承这个概念也不多说,面向对象的基础知识,直接先看两种语言实现继承的方式

Ruby 通过 < 父类名 实现继承,示例代码:

class Car
	def drive
		p "car start.."
	end
end

class SuperCar < Car
	def speed_up
		p "speed to 200km ..."
	end
end

s = SuperCar.new
s.drive
s.speed_up

#=> "car start.."
#=> "speed to 200km ..."

Java 通过 extends实现继承的,示例代码:

class Car {
    void drive(){
        System.out.println("Car Start ...");
    }
}

public class SuperCar extends Car {

    void speedUp() {
        System.out.println("speed to 200km...");
    }

    public static void main(String[] args) {
        SuperCar c = new SuperCar();
        c.drive();
        c.speedUp();
    }
}
// Car Start ...
// speed to 200km...

关于类的继承方面我们可以得出以下总结:

  • Ruby 通过 < 实现继承, Java 通过 extends 关键字实现继承
  • Ruby ,Java 在类没有指定父类的情况下都默认继承 Object

关于继承还有一些经验分享的就是,继承的特性更多用于重写父类和多态,如果是想要复用公共的功能,但是类之类没有明显的继承关系的话,就应该遵循组合优先大于继承的原则,不过在 Ruby 中很好的通过 Mix-in 扩展解决的继承这个问题

模块和Mix-in

模块使用 module 关键字创建,命名规则和类一样,首字母必须大写,我们先来看看如何创建模块

module Display
	def open
		p "open display..."
	end
end

Display.open	# private method `open' called for Display:Module (NoMethodError)

模块是 Ruby 的特色功能,定位也很明确,有以下几个特点:

  • 不能拥有实例,不能被继承,所以模块定位清晰,仅仅表示事物的通用行为

  • 函数仅仅只能在内部被调用,除非使用 module_function 声明模块函数

  • 模块更多是结合 Mix-ininclude 使用,为类提供增强和更多的可能性

Ruby 中的模块提供的命名空间 namespace 概念就跟 Java 的包(Package)类似,都是用于区分相同的类,常量,Mix-in 结合 include 也就类似 Java 里面的静态导入,在 Java 中 import static 可以无需声明包路径直接调用导入类的变量和方法,所谓的 Mix-in 就是利用模块的抽象能力把非继承关系的类之间的共性进行抽象和复用,有些类似 AOP 的概念,可以使代码既强大又灵活

当我们用 OOP 思想对现实进行抽象的时候,会发现很多非继承关系又存在共同功能的事物,例如智能手机,手表继承自不同的父类,但是他们又都有看时间 watch_time的能力,我们用代码展现这种继承关系,请看示例代码:

class Phone
	def call
		p "phone call.."
	end
end

class IPhone < Phone
	# iPhone 继承自 Phone 类,但是不仅可以打电话,还可以看时间
	def watch_time
		p "watch_time 12:00:000"
	end
end

class Watch
	# 手表都可以看时间
	def watch_time
		p "watch_time 12:00:000"
	end
end

class AppleWatch < Watch
	# AppleWatch 不仅看时间,还有运动的功能
	def run
		p "start run"
	end
end

结合上面的代码,我们可以看到 watch_time 代码重复,对于不同继承体系但是相同功能的时候,就可以用 Mix-in 解决这个问题,思路如下:

  • 将例如 watch_time 相同的方法和代码,抽出定义在 module 模块中
  • 使用 include 引入模块,将方法引入到实际的类中

使用 Mix-in 后我们可以看下代码变化,示例代码:

module WatchTime
	# 抽出通用的方法
	def watch_time
		p "watch_time 12:00:000"
	end
end

class Phone
	def call
		p "phone call.."
	end
end

class IPhone < Phone
	include WatchTime
end

class Watch
	include WatchTime
end

class Apple_watch < Watch
	# apple_watch 不仅看时间,还可以运动
	def run
		p "start run"
	end
end

使用 Mix-in 这种灵活的语法实现了鱼和熊掌兼得,即没有破坏继承结构关系又实现共性方法的代码复用问题,因为 Java 没有 Mix-in 的概念所以就不展示示例代码了,不过 Java 也有自己的解决方案,而且在 Java 的解决代码复用问题通常都应该遵循 组合大于继承 的原则,因为 Java 的语言设计让继承更多用于多态而非复用

运算符

简单说一下运算符,虽然大多编程语言的运算符非常的简单,赋值运算,逻辑运算,条件运算符所有语言的使用方式都几乎差不多,好像没什么好讲的,但 Ruby 灵活的语法是有不少语法糖,还是可以 Java 程序员羡慕的一下的,假设一张我们在业务代码中经常遇到的情况,根据表达式取值,当表达式为 true 时改变变量的值,这种简单逻辑赋值在 Java 只能这样写,请看示例代码

String value = "abc";
if (condition != null) {
  value = condition;
}
// 看上去很啰嗦

这种情况在 Ruby 中一行代码可以实现相同语义:

# 当 condition 表达式为 true 执行 value = condition , 否则执行 value = "abc"
value = condition || "abc"

只所以可以实现是因为 Ruby 有一个不同 Java 的特定, Ruby 对象都可以用于进行布尔表达式判断,判断逻辑为**对象本身不为 nil 或者 false 表达式则为 true,否则为 false **

还有一种逻辑则是取相反的情况,例如我们经常遇到一种情况是,判断数组不为空的时候取数组的某一个下标,在 Java 中只能这样写,示例代码

List<String> list = Arrays.asList("a", "b", "c");
String item = null;
if (list != null) {
        item = list.get(0);
}
// "a"

这种情况可以用逻辑运算符 &&, 它刚好与上面 || 相反,也是一行代码可以实现相同功能

str_list = ["a", "b", "c"]
item = str_list && str_list[0]
#=> "a"

我个人非常喜欢这种简洁的写法,不过建议在多人项目中不要用太多语法糖,不然可能会造成项目代码可读性混乱

异常处理

很多程序员大部分时间都花在查错上,所以迅速定位异常很关键,先看看 Ruby 的异常格式 文件名:行号:in 方法名:错误信息(异常类名) 简单的用法就不写示例代码了,不然占用太多篇幅,两种语言处理异常方法大同小异,具体处理方式有如下区别:

  • Ruby 处理异常使用 begin ~ rescue ~ ensure ~ end 这里太简单就不写示例代码了
  • Java 7 使用 try ~ catch ~ finally 到 Java 8 后有了更高效的 try ~ with ~ resources 可自动关闭资源

不过 Ruby 的 Retry 倒是 Java 没有的特性,它适合对一些容易出错的程序(例如调用外部 API )可以进行快速重试,具体可以请看示例代码

class HandleException
	def zero
		x, y = 100, 0
		begin
			x = x / y
		rescue
			p '执行异常逻辑'
			y = 2
			retry
		end
		x
	end
end

h = HandleException.new
p h.zero

#=>"执行异常逻辑"
#=>50

上述程序非常简单,大概逻辑是首次执行会抛出异常,然后被 rescue 捕获后重新复制,第二次运算成功,Java 如果要实现相同的语义的话,则代码没有这么简洁了,跟上章节的逻辑运算符 &&|| 类似 resuce 也具备条件表达式的运算,具备很强的表达能力,我们尝试对以上述代码进行一些简化,示例代码:

x, y = 100, 0
x = x / y rescue 50
# => x = 50

当运算 x / y 没有出现异常则运算 x = x / y,当出现异常被 resuce 捕获后则运算 x = 50,但相同逻辑在 Java 里面就会显得有点啰嗦,请看示例代码:

int x = 100, y = 0;
try {
    x = x / y;
}catch (ArithmeticException ae) {
    x = 50;
}
// x = 50

不过像这种小技巧建议只用于简单的场景,如果业务流程复杂,为了保证代码清晰易懂还是建议使用标准的 begin ~ rescue ~ ensure ~ end 异常处理语句 ,异常章节到此结束,在文章尾部我们总结一下 Java 和 Ruby 在异常处理的区别:

  • Ruby 标准异常库都是继承 Exception 类,程序通常只能处理 StandarError 异常或其子类
  • Java 异常都是继承 Throwable ,异常被划分为 Error 异常和 Exception,程序通常只能处理 Exception 的子类 RuntimeException 以及其子类
  • Ruby 支持 retry 从异常中快速重试,rescue 表达式简化异常代码处理,Java 则没有该功能
  • Java 主动抛异常的使用 throw new Exception,而 Ruby 则使用 raise 方法

image-20200427221028455

两种语言的基本语法对比就先写到这里,暂时就不写分析和总结了,因为我后续还会继续探索 RubyJava 在其他使用层面的使用区别对比,例如字符串,数据类型,集合,哈希,最后想留一个问题:你觉得静态语言和动态语言最明显的区别在哪里?他们各自会存在什么问题?在什么场景下你会偏向动态语言,什么场景你会偏向静态语言?

posted @ 2020-04-29 22:32  肖卫卫讲编程  阅读(1013)  评论(2编辑  收藏  举报