當前篇:全民 Kotlin:你沒有玩過的全新玩法
第三篇:全民 Kotlin:協程特別篇
本文章已授權鴻洋微信公眾號轉載
目錄
空安全
方法支持添加默認參數
方法上面的參數不可變
類方法擴展
函數變量
內聯函數
-
委托機制
類委托
屬性委托
懶委托
-
高階函數
let 函數
with 函數
run 函數
apply 函數
also 函數
運算符重載
空安全
在 Java 不用強制我們處理空對象,所以常常會導致 NullPointerException 空指針出現,現在 Kotlin 對空對象進行了限定,必須在編譯時處理對象是否為空的情況,不然會導致編譯不通過
在對象不可空的情況下,可以直接使用這個對象
fun getText() : String {
return "text"
}
val text = getText()
print(text.length)
- 在對象可空的情況下,必須要判斷對象是否為空
fun getText() : String? {
return null
}
val text = getText()
if (text != null) {
print(text.length)
}
// 如果不想判斷是否為空,可以直接這樣,如果 text 對象為空,則會報空指針異常,一般情況下不推薦這樣使用
val text = getText()
print(text!!.length)
// 還有一種更好的處理方式,如果 text 對象為空則不會報錯,但是 text.length 的結果會等于 null
val text = getText()
print(text?.length)
方法支持添加默認參數
- 在 Java 方法上,我們可能會為了擴展某個方法而進行多次重載
public void toast(String text) {
toast(this, text, Toast.LENGTH_SHORT);
}
public void toast(Context context, String text) {
toast(context, text, Toast.LENGTH_SHORT);
}
public void toast(Context context, String text, int time) {
Toast.makeText(context, text, time).show();
}
toast("彈個吐司");
toast(this, "彈個吐司");
toast(this, "彈個吐司", Toast.LENGTH_LONG);
- 但是在 Kotlin 上面,我們無需進行重載,可以直接在方法上面直接定義參數的默認值
fun toast(context : Context = this, text : String, time : Int = Toast.LENGTH_SHORT) {
Toast.makeText(context, text, time).show()
}
toast(text = "彈個吐司")
toast(this, "彈個吐司")
toast(this, "彈個吐司", Toast.LENGTH_LONG)
方法上面的參數不可變
在 Java 方法上面,我們可以隨意修改方法上面參數的賦值,但是到了 Kotlin 這里是不行的,Kotlin 方法參數上面的變量是 val (對應 Java 的 final)類型的,那么這個時候我們有兩種解決方案:
第一種,在方法里面定義一個一模一樣的變量,具體寫法如下:
class XxxView : View {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var widthMeasureSpec: Int = widthMeasureSpec
var heightMeasureSpec: Int = heightMeasureSpec
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
}
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
}
但是編譯器會報警告,提示我們出現了重復變量,但是仍可正常編譯和運行,所以不推薦這種寫法
第二種,在方法里面定義一個不同名稱的變量,具體寫法如下:
class XxxView : View {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var finalWidthMeasureSpec: Int = widthMeasureSpec
var finalHeightMeasureSpec: Int = heightMeasureSpec
if (MeasureSpec.getMode(finalWidthMeasureSpec) == MeasureSpec.AT_MOST) {
finalWidthMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
}
if (MeasureSpec.getMode(finalHeightMeasureSpec) == MeasureSpec.AT_MOST) {
finalHeightMeasureSpec = MeasureSpec.makeMeasureSpec(30, MeasureSpec.EXACTLY)
}
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
}
}
其實就在原來的基礎上加一個 final 前綴,這樣不僅解決了編譯器警告的問題,還解決了我們還要重新想一個名稱來給變量命名的煩惱。
那么肯定有人會問了,有沒有辦法像 Java 一樣改呢?關于這個問題我也糾結了一陣子,但是查閱了很多文檔和資料,最終發現并沒有辦法,所以只能妥協了,畢竟這個世界上沒有什么事物是完美的。
類方法擴展
- 可以在不用繼承的情況下對擴展原有類的方法,例如對 String 類進行擴展方法
fun String.handle() : String {
return this + "Android輪子哥"
}
// 需要注意,handle 方法在哪個類中被定義,這種擴展只能在那個類里面才能使用
print("HJQ = ".handle())
HJQ = Android輪子哥
函數變量
- 在 Kotlin 語法中函數是可以作為變量進行傳遞的
var result = fun(number1 : Int, number2 : Int) : Int {
return number1 + number2
}
- 使用這個函數變量
println(result(1, 2))
內聯函數
- 有人可能會問了,內聯函數是蝦米?我舉個栗子,用 Kotlin 編寫以下代碼
class Demo {
fun test() {
showToast("666666")
}
/**
* 這個就是我們今天的主角:內聯函數了,用 inline 關鍵字來修飾
*/
private inline fun showToast(message: String) {
ToastUtils.show(message)
}
}
- 經過反編譯之后,會變成以下代碼:
/* compiled from: Demo.kt */
public final class Demo {
public final void test() {
ToastUtils.show("666666");
}
}
看到這里相信大家應該知道內聯函數的用法和作用了,內聯函數就是在編譯的時候將所有調用 inline 函數的代碼直接替換成方法里面的代碼,那么大家可能有疑問了,這樣做有什么實際好處呢?它其實提升了代碼的性能,這跟基本數據類型的常量會在編譯的過程中被優化一樣,但是如果 inline 函數被許多處地方調用,并且 inline 函數的實現代碼比較多的情況下,也會相應導致代碼量增加。
另外上面的代碼示例中,編譯器在 inline 關鍵字上面有一個代碼警告,原話是這樣的:
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
內聯對性能的預期影響是微不足道的。內聯最適用于參數為函數類型的函數
- 大致的意思是,上面的代碼示例中,內聯函數能起到的性能優化是微不足道的,它比較適合帶有 lambda 參數的函數,根據這個提示,將上面的代碼示例修改成下面這樣就不會報代碼警告了:
class Demo {
fun test() {
showToast({
println("測試輸出了")
}, "7777777")
}
private inline fun showToast(function: () -> Unit, message: String) {
function.invoke()
ToastUtils.show(message)
}
}
- 有人可能會好奇了,這樣就能有很大的性能提升?有什么判斷依據呢?接下來讓我們做一組實驗,加 inline 和不加 inline 反編譯出來的代碼有什么區別,先來看加 inline 之后反編譯出來的代碼長啥樣
/* compiled from: Demo.kt */
public final class Demo {
public final void test() {
System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
ToastUtils.show("7777777");
}
}
- 一切都在預料之中,那么不加 inline 反編譯出來又是什么效果呢?
/* compiled from: Demo.kt */
public final class Demo {
public final void test() {
showToast(1.INSTANCE, "7777777");
}
private final void showToast(Function0<Unit> function, String message) {
function.invoke();
ToastUtils.show(message);
}
}
/* compiled from: Demo.kt */
final class Demo$test$1 extends Lambda implements Function0<Unit> {
public static final Demo$test$1 INSTANCE = new Demo$test$1();
Demo$test$1() {
super(0);
}
public final void invoke() {
System.out.println("\u6d4b\u8bd5\u8f93\u51fa\u4e86");
}
}
很明顯,不加 inline 會導致多生成一個內部類,這個是 lambda 函數多出來的類,并且里面的示例還是靜態,這無疑會增加內存消耗,另外這樣還有另外一個好處,就是能少一層方法棧的調用。
除了 inline (內聯)這個關鍵字,還有另外一個關鍵字:noinline(禁止內聯),大家可能到這里就摸不著頭腦了,這個有啥用?我不在方法上面寫 inline 不就是不會內聯了么?那么這個關鍵字是有什么作用呢?其實這個關鍵字不是修飾在方法上面的,而是修飾 在 lambda 參數上面的,假設一個 inline 函數上面有多個 lambda 參數,那么我只想對某個 lambda 參數內聯,其他 lambda 參數不內聯的情況下,就可以使用這個關鍵字來對不需要進行內聯的 lambda 參數進行修飾,大體用法如下:
private inline fun showToast(function1: () -> Unit, noinline function2: () -> Unit, message: String) {
function1.invoke()
function2.invoke()
ToastUtils.show(message)
}
密封類
- 大家看到這個詞語的時候,第一反應是,密封類是啥子?大家應該用過枚舉吧?先說枚舉類的幾個弊端,第一個枚舉值賦值是固定的(一旦賦值之后就不可變),第二個枚舉值的類型是固定的(類型只能是自己),密封類的出現正是為了解決這兩個問題,具體用法如下:
sealed class Result {
// 定義請求成功
data class SUCCESS(val data: String) : Result()
// 定義請求失敗
data class FAIL(val throwable: Throwable) : Result()
}
val result = if (AppConfig.isDebug()) {
Result.SUCCESS("模擬后臺返回數據")
} else Result.FAIL(IllegalStateException("模擬請求失敗了"))
when (result) {
is Result.SUCCESS -> {
println(result.data)
}
is Result.FAIL -> {
println(result.throwable)
}
}
- 從這里可以看到,密封類和枚舉類很類似,但是比枚舉類更加強大,枚舉可以是任意類型(
SUCCESS
類型或者FAIL
類型),一個枚舉值可以有很多種結果(FAIL
類中的throwable
參數是外層傳入的,而不是像枚舉一樣只能固定在內部)。
委托機制
類委托
- 先讓我們來看一段代碼
// 定義日志策略接口
interface ILogStrategy {
fun log(message: String)
}
// 實現一個默認的日志策略類
class LogStrategyImpl : ILogStrategy {
override fun log(message: String) {
Log.i("測試輸出", message)
}
}
// 創建一個日志代理類
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy
-
看到這里大家可能有一些疑惑
ILogStrategy by strategy
是蝦米操作?LogStrategyProxy
這個類不去實現接口方法難道不會導致編譯不通過么?
關于這兩個問題,我覺得都可以用同一個解釋,LogStrategyProxy 之所以不用實現 ILogStrategy 的 log 方法,是因為在
ILogStrategy
接口后面加了by strategy
,而strategy
對象就是 LogStrategyProxy 構造函數中的變量,意思是讓這個接口的具體實現由strategy
對象幫我實現就可以了,我(LogStrategyProxy
類)不需要再實現一遍了,這樣是不是跟 Java 中的靜態代理很像?只不過在 Kotlin 類委托特性上面編譯器幫我們自動生成接口方法的代碼,你可以把它想象下面這樣的代碼:
class LogStrategyProxy(val strategy: ILogStrategy) : ILogStrategy {
override fun log(message: String) {
strategy.log(message)
}
}
有人肯定會問了:口說無憑,我憑什么相信你就是這樣的代碼?
這是個好問題,我提供一下反編譯之后的代碼,大家看一下就能明白了:
public final class LogStrategyProxy implements ILogStrategy {
private final /* synthetic */ ILogStrategy $$delegate_0;
public LogStrategyProxy(@NotNull ILogStrategy strategy) {
Intrinsics.checkNotNullParameter(strategy, "strategy");
this.$$delegate_0 = strategy;
}
public void log(@NotNull String message) {
Intrinsics.checkNotNullParameter(message, "message");
this.$$delegate_0.log(message);
}
}
- 是不是就立馬頓悟了?調用的話也很簡單,代碼如下:
val logStrategyImpl = LogStrategyImpl()
LogStrategyProxy(logStrategyImpl).log("666666")
- 最后讓我們看看輸出的日志:
測試輸出: 666666
- 這個我突然有一個大膽的想法,在使用類委托的情況下,再去重寫它的接口方法呢?例如下面的:
class LogStrategyProxy(strategy: ILogStrategy) : ILogStrategy by strategy {
override fun log(message: String) {
println("測試輸出 " + message)
}
}
- 關于這個問題我已經做過實踐了,是木有問題的,大家放心大膽搞。
屬性委托
- 看過了上面的類委托,想必大家對委托有一定的了解了,那么屬性委托是什么呢?簡單來講,類委托是為了幫我們減少一些實現代碼,而屬性委托是為了幫我們控制變量的 Get、Set 的操作了,廢話不多說,下面演示一下用法,下面先創建一個委托類
class XxxDelegate {
// 先給它一個默認值
private var currentValue: String = "666666"
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("測試字段名為 ${property.name} 的變量被訪問了,當前值為 $currentValue")
return currentValue
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
currentValue = newValue
println("測試字段名為 ${property.name} 的變量被賦值了,當前值為 $currentValue" + ",新的值 $newValue")
}
}
- 使用代碼示例如下:
var temp: String by XxxDelegate()
println("測試輸出 " + temp)
temp = "55555"
println("測試輸出 " + temp)
- 具體日志輸出如下:
System.out: 測試字段名為 temp 的變量被訪問了,當前值為 666666
System.out: 測試輸出 666666
System.out: 測試字段名為 temp 的變量被賦值了,當前值為 55555,新的值 55555
System.out: 測試字段名為 temp 的變量被訪問了,當前值為 55555
System.out: 測試輸出 55555
- 看到這里你是否明白了,這個
XxxDelegate
類里面只有兩個方法,一個是getValue
,另外一個是setValue
,從方法命名上我們已經能大致得出它的作用了,這里就不再多講解了,var temp: String by XxxDelegate()
表示這個temp
對象的創建會全權委托給XxxDelegate
這個類來做。
懶委托
- 什么是懶委托呢?大家知道單例模式中的懶漢式吧?這個跟它差不多,只不過我們不需要寫靜態方法和鎖機制了,只需要像下面這樣寫:
val temp: String by lazy {
println("測試變量初始化了")
return@lazy "666666"
}
- 調用代碼如下:
println("測試開始")
println("測試第一次輸出 " + temp)
println("測試第二次輸出 " + temp)
println("測試結束")
- 輸出日志如下:
System.out: 測試開始
System.out: 測試變量初始化了
System.out: 測試第一次輸出 666666
System.out: 測試第二次輸出 666666
System.out: 測試結束
- 是不是真的跟懶漢式差不多?只不過這種寫法簡化了很多,另外在日常開發中我們可以用它來做
findViewById
是最適合不過的。
private val viewPager: ViewPager? by lazy { findViewById(R.id.vp_home_pager) }
-
另外懶委托還提供了幾種懶加載模式供我們選擇,
LazyThreadSafetyMode.SYNCHRONIZED:同步模式,確保只有單個線程可以初始化實例,這種模式下初始化時線程安全的,當
by lazy
沒有指定模式的時候,就是默認用的這種模式。LazyThreadSafetyMode.PUBLICATION:并發模式,在多線程下允許并發初始化,但是只有第一個返回的值作為實例,這種模式下是線程安全的,和
LazyThreadSafetyMode.SYNCHRONIZED
最大區別是,這種模式在多線程并發訪問下初始化效率是最高的,本質上面是用空間換時間,哪個的線程執行快就讓哪個先返回結果,其他線程執行的結果拋棄掉。LazyThreadSafetyMode.NONE:普通模式,這種模式不會使用鎖來限制多線程訪問,所以是線程不安全的,所以請勿在多線程并發的情況下使用。
具體使用的方式也很簡單,如下:
val temp: String by lazy(LazyThreadSafetyMode.NONE) {
println("測試變量初始化了")
return@lazy "666666"
}
- 另外有一點需要注意,使用懶委托的變量必須聲明為 val(不可變的),因為它只能被賦值一次。
高階函數
- 高階函數是 Kotlin 用于簡化一些代碼的寫法,提升代碼可讀性而產生的,其中有 let、with、run、apply、also 五個常用函數
let 函數
在函數塊內可以通過 it 指代該對象。返回值為函數塊的最后一行或指定 return 表達式
一般寫法
fun main() {
val text = "Android輪子哥"
println(text.length)
val result = 1000
println(result)
}
- let 寫法
fun main() {
val result = "Android輪子哥".let {
println(it.length)
1000
}
println(result)
}
- 最常用的場景就是使用let函數處理需要針對一個可 null 的對象統一做判空處理
videoPlayer?.setVideoView(activity.course_video_view)
videoPlayer?.setControllerView(activity.course_video_controller_view)
videoPlayer?.setCurtainView(activity.course_video_curtain_view)
videoPlayer?.let {
it.setVideoView(activity.course_video_view)
it.setControllerView(activity.course_video_controller_view)
it.setCurtainView(activity.course_video_curtain_view)
}
- 又或者是需要去明確一個變量所處特定的作用域范圍內可以使用
with 函數
前面的幾個函數使用方式略有不同,因為它不是以擴展的形式存在的。它是將某對象作為函數的參數,在函數塊內可以通過 this 指代該對象,返回值為函數塊的最后一行或指定 return 表達式
定義 Person 類
class Person(var name : String, var age : Int)
- 一般寫法
fun main() {
var person = Person("Android輪子哥", 100)
println(person.name + person.age)
var result = 1000
println(result)
}
- with 寫法
fun main() {
var result = with(Person("Android輪子哥", 100)) {
println(name + age)
1000
}
println(result)
}
- 適用于調用同一個類的多個方法時,可以省去類名重復,直接調用類的方法即可,經常用于 Android 中
RecyclerView.onBinderViewHolder
中,數據 model 的屬性映射到 UI 上
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
holder.nameView.text = "姓名:${item.name}"
holder.ageView.text = "年齡:${item.age}"
}
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
with(item){
holder.nameView.text = "姓名:$name"
holder.ageView.text = "年齡:$age"
}
}
run 函數
實際上可以說是 let 和 with 兩個函數的結合體,run 函數只接收一個 lambda 函數為參數,以閉包形式返回,返回值為最后一行的值或者指定的 return 的表達式
一般寫法
var person = Person("Android輪子哥", 100)
println(person.name + "+" + person.age)
var result = 1000
println(result)
- run 寫法
var person = Person("Android輪子哥", 100)
var result = person.run {
println("$name + $age")
1000
}
println(result)
- 適用于 let,with 函數任何場景。因為 run 函數是let,with兩個函數結合體,準確來說它彌補了 let 函數在函數體內必須使用 it 參數替代對象,在 run 函數中可以像 with 函數一樣可以省略,直接訪問實例的公有屬性和方法,另一方面它彌補了 with 函數傳入對象判空問題,在 run 函數中可以像l et 函數一樣做判空處理,這里還是借助 onBindViewHolder 案例進行簡化
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
holder.nameView.text = "姓名:${item.name}"
holder.ageView.text = "年齡:${item.age}"
}
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
item?.run {
holder.nameView.text = "姓名:$name"
holder.ageView.text = "年齡:$age"
}
}
apply 函數
從結構上來看 apply 函數和 run 函數很像,唯一不同點就是它們各自返回的值不一樣,run 函數是以閉包形式返回最后一行代碼的值,而 apply 函數的返回的是傳入對象的本身
一般寫法
val person = Person("Android輪子哥", 100)
person.name = "HJQ"
person.age = 50
- apply 寫法
val person = Person("Android輪子哥", 100).apply {
name = "HJQ"
age = 50
}
- 整體作用功能和 run 函數很像,唯一不同點就是它返回的值是對象本身,而 run 函數是一個閉包形式返回,返回的是最后一行的值。正是基于這一點差異它的適用場景稍微與 run 函數有點不一樣。apply 一般用于一個對象實例初始化的時候,需要對對象中的屬性進行賦值。或者動態 inflate 出一個 XML 的 View 的時候需要給 View 綁定數據也會用到,這種情景非常常見。特別是在我們開發中會有一些數據 model 向 View model 轉化實例化的過程中需要用到
mRootView = View.inflate(activity, R.layout.example_view, null)
mRootView.tv_cancel.paint.isFakeBoldText = true
mRootView.tv_confirm.paint.isFakeBoldText = true
mRootView.seek_bar.max = 10
mRootView.seek_bar.progress = 0
- 使用 apply 函數后的代碼是這樣的
mRootView = View.inflate(activity, R.layout.example_view, null).apply {
tv_cancel.paint.isFakeBoldText = true
tv_confirm.paint.isFakeBoldText = true
seek_bar.max = 10
seek_bar.progress = 0
}
- 多層級判空問題
if (sectionMetaData == null || sectionMetaData.questionnaire == null || sectionMetaData.section == null) {
return;
}
if (sectionMetaData.questionnaire.userProject != null) {
renderAnalysis();
return;
}
if (sectionMetaData.section != null && !sectionMetaData.section.sectionArticles.isEmpty()) {
fetchQuestionData();
return;
}
- kotlin 的 apply 函數優化
sectionMetaData?.apply {
// sectionMetaData 對象不為空的時候操作sectionMetaData
}?.questionnaire?.apply {
// questionnaire 對象不為空的時候操作questionnaire
}?.section?.apply {
// section 對象不為空的時候操作section
}?.sectionArticle?.apply {
// sectionArticle 對象不為空的時候操作sectionArticle
}
also 函數
- also 函數的結構實際上和 let 很像唯一的區別就是返回值的不一樣,let 是以閉包的形式返回,返回函數體內最后一行的值,如果最后一行為空就返回一個 Unit 類型的默認值。而 also 函數返回的則是傳入對象的本身
fun main() {
val result = "Android輪子哥".let {
println(it.length)
1000
}
println(result) // 打印:1000
}
fun main() {
val result = "Android輪子哥".also {
println(it.length)
}
println(result) // 打印:Android輪子哥
}
- 適用于 let 函數的任何場景,also 函數和 let 很像,只是唯一的不同點就是 let 函數最后的返回值是最后一行的返回值而 also 函數的返回值是返回當前的這個對象。一般可用于多個高階函數鏈式調用
運算符重載
- 在 Kotlin 中使用運算符最終也會調用對象對應的方法,我們可以通過重寫這些方法使得這個對象支持運算符,這里不再演示代碼
運算符 | 調用方法 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
運算符 | 調用方法 |
---|---|
a++ | a.inc() |
a-- | a.dec() |
運算符 | 調用方法 |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b), a.mod(b) (deprecated) |
a..b | a.rangeTo(b) |
運算符 | 調用方法 |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
運算符 | 調用方法 |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
運算符 | 調用方法 |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
運算符 | 調用方法 |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b), a.modAssign(b) (deprecated) |
運算符 | 調用方法 |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
運算符 | 調用方法 |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |