Java核心技术卷Ⅰ 读书分享 3、4、6章

第三章Java的基本程序设计结构

数据类型

Java 是一种强类型语言,必须为每个变量声明一种类型。在 Java 中有 8 种基本类型:4 种整型,2种浮点型,1 种用于表示 Unicode 编码的字符单元的字符类型 char,和一种用于表示真值的 boolean 类型(String 和 数组都是对象)。

整型:

用于表示没有小数部分的数值,它允许是负数
取值范围 -2^(位-1) -----2^(位-1)-1 一个字节代表8位

类型 存储需求 取值范围 具体值
int 4 字节 -2的32次方~2的32次方-1 -2147483646-2147483647(正好超过20亿)
short 2 字节 -2的16次方~2的16次方-1 -32768~32767
long 8 字节 -2的64次方~2的64次方-1 -9223372036854775808~9223372036854775807
byte 1 字节 -2的8次方~2的8次方-1 -128~127

在 Java 种,整型的范围于运行 Java 代码的机器无关。
长整型数值后面有一个 L 后缀,从 Java7 开始,加上前缀 0b 或 0B 就可以写二进制数。如 0b1001 就代表 9,还可以为数字字面量加下划线,如用 1_000_000 表示一百万,更加可读,编译器会去掉这些下划线。

System.out.println(0b1001); //  result : 9
System.out.println(1_000_000); // result :1000000

浮点类型

用于表示有小数部分的数值。Java 中有两种浮点类型。

类型 存储需求 取值范围
float 4 字节 -1.7乘以10的38次方~1.7乘以10的38次方
double 8 字节 -3.4乘以10的308次方~3.4乘以10的308次方

double 表示这种类型的数值精度是 float 类型的两倍(也有人称之为双精度数值),float 类型的数值后有一个后缀 F 或 f,没有后缀的默认为 double 类型,double 类型也可以加后缀 D 或 d 。
注意浮点类型计算存在误差,是因为浮点数值采用二进制系统表示,在二进制系统中无法精确地表示分数 1/10 ,可以使用 BigDecimal 类代替实现。

System.out.println(2.0-1.1);
result : 0.8999999999999999

char 类型

char 类型用于表示单个字符,不过现在有些变化,有些 Unicode 字符可以用一个 char 值描述,另外一些 Unicode 字符则需要两个 char 值。'A' 与 "A" 不同,前者是编码值为 65 所对应的字符常量,后者是包含一个字符 A 的字符串。char 类型的值可以表示为十六进制值,其范围从 \u0000 到 \Uffff 。
下面这行代码是符合语法标准的,\u005B 表示 [\u005D 表示 ]。Unicode 转义序列会在解析代码之前得到处理。

public static void main(String\u005B\u005D args) {}
image.png
微信截图_20190423100013.png

在IDEA中
image.png

Java中可以使用 \u + Unicode编码来进行转义,如 \u0022,除了这个以外,还有一些特殊的转义序列

转义序列 名称 Unicode值
\b 退格 \u0008
\t 制表 \u0009
\n 换行 \u000a
\r 回车 \u000d
\" 双引号 \u0022
\' 单引号 \u0027
\\ 反斜杠 \u005c

boolean 类型

boolean(布尔) 类型有两个值:false 和 true,用来判定逻辑条件。整型值和布尔值之间不能进行相互转换。

变量

在 Java 中,每个变量都一个类型(type)。在声明变量时,变量的类型位于变量名之前,例如:

double salary;
int vacationDays;
boolean done;

变量名必须要以字母开头,并由字母或数字组成的序列,不过这里的“字母”的概念不单指英文字母,字母包括 A~Za~z_$,或在某种语言中表示字母的任何 Unicode 字符,比如德国人就可以在变量名中使用字母 ä (读音为:ei)。

常量

在 Java 中,利用关键字 final 指示常量,例如
final int cout = 3 ;
关键字 final 表示这个变量只能被赋值一次,一旦被赋值之后,就不能够再修改了,习惯上,常量名使用全大写。声明在类中,用 static final 声明的变量,也被称为类常量

运算符

在 Java 中,使用算术运算符 +,-,*,/,表示加减乘除运算,当参与/运算的两个操作数都是整数时,表示整数触发;否则表示浮点除法。整数的求余操作(有时称为取模),用 % 表示。

a = 15 , b = 2   a/b=7
a%b = 1;
a=15.0
a/b = 7.5

数学函数与常量

Math 类中包含了各种各样的数学函数,比如这里有一个计算数值平方根的方法

double x = 4;
double y = Math.sqrt(x);// sqrt 接受一个 double 值
System.out.println(y);

幂运算的方法

// y 的值为 x 的 a 次幂,同样接受 double 值。
double y = Math.pow(x,a);

数值类型之间的转换

如果两个操作数中有一个是 double 类型,另一个操作数就会转换为 double 类型。
否则,如果其中一个操作数是 float 类型,另一个操作数将会转换为 float 类型。
否则,如果其中一个操作数是 long 类型,另一个操作数将会转换为 long 类型。
否则,两个操作数都将被转换为 int 类型。

结合赋值和运算符

"+=",“-=”,“*=”,“%=”,这些都是在赋值中使用二元运算符,但是不会改变数据的类型,例如:

int x  =2 ;
x+=3.5;
此时等价于: x = (int)(x+3.5)//先变成 double ,再被转换为 int 

自增与自减运算符

++nn++,是两种不同的含义,如果把加号放在前缀,那么则会先自增,再运算表达式的值;如果放在后缀,那么则会先运算表达式的值,再自增。另外,++4,是错误的,自增与自减运算符只能用于变量,不能是数值。

int a = 2;
int c = 3;
System.out.println(a++);
System.out.println(++c);
--------------------------
2
4

关系和 boolean 运算符

  • == :检测相等性
  • !=:检测不相等
  • <,>,<=,>=:小于,大于,小于等于,大于等于
  • &&:采用短路的做法,如果前者为 false ,则不计算后者
  • ||:采用短路的做法,如果前者为 true ,则不计算后者
  • ?: :三元运算符,condition? expression1:expression2,如果 condition 为 true,就为第一个表达式的值,反之则为第二个表达式的值。

位运算符

  • &:&在运算的时候,将2个数字的二进制做比较,当2个数字的值都为1时,才为1,否则就是0
  • |:充当 数值运算符的时候 同样是比较2进制,当有一个数为1,那么就取1
  • ^:充当 数值运算符的时候 同样是比较2进制, 只能有1个1,那就取1
  • ~:取反值
  • >>:补最左边的数位时,会根据符号位, 符号是1 就填充1,符号是0,就填充0;
  • <<:左移
  • >>>:无符号右移,:对于正数 有符号与无符号的右移没有区别。 对于负数 来说,不管你是0还是1,都会用0去补位

括号与运算符级别

如果不使用括号,就按照给出的运算符优先级次序进行计算,同一个级别的运算符按照从左到右的次序进行是计算(除了右结合运算符),

运算符 结合性
[] .()(方法调用) 从左向右
! ~ ++ -- +(一元运算) -(一元运算) ()(强制类型转换) new 从右向左
* / % 从左向右
+(正) -(负) 从左向右
<< >> >>> 从左向右
< <= > >= instanceof 从左向右
== != 从左向右
& 从左向右
^ 从左向右
| 从左向右
&& 从左向右
|| 从左向右
?: 从右向左
= += -= *= /= %= &= ^= <<= >>= >>>= 从右向左

字符串概念

检测字符串是否相等

使用 equals 方法检测两个字符串是否相等,s.equals(t),如果字符串 s 与字符串 t 相等,则返回 true,否则,返回 false。s 和 t 可以是字符串变量,也可以是字符串字面量:

String abc = "Hello";
abc.equals("Hello");
"Hello".equals(abc);

如果你希望检测两个字符串是否相等,而不区分大小写,可以使用 equalsIgnoreCase 方法。不能使用 == 来比较字符串是否相同,因为 == 比较的是变量的内存地址,而不是变量的值。

空串与 Null 串

空串 "" 是长度为0的字符串,可以调用以下代码检查一个字符串是否为空。

if(str.length()==0)
if(str.equals(""))

空串是一个 Java 对象,有自己的串长度 (0) 和内容 (空) ,不过 String 变量还可以存放一个特殊值:null,表示目前没有任何对象与该变量关联,要检查一个字符串是否为 null,可以使用以下条件:
if (str == null)
有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:
if (str !=null && str.length() != 0)

String API

  • boolean equals(Object other)
  • boolean equalsIgnoreCase(String other)
  • boolean startWith(String prefix) 如果字符串以 prefix 开头,则返回 true
  • boolean endsWith(String suffix) 如果字符串以 suffix 结尾,则返回 true
  • int indexOf(String str)
  • int indexOf(String str,int fromIndex)
  • int indexOf(int cp)
  • int indexOf(int cp,int fromIndex)
  • int lastIndexOf(String str)
  • int lastIndexOf(String str,int fromIndex)
  • int lastIndexOf(int cp)
  • int lastIndexOf(int cp,int fromIndex)
  • length()
  • String replace(CharSequence oldString,CharSequence newString),可以用 String 或 StringBuilder 对象作为 CharSequence 参数。
  • String substring(int beginIndex)
  • String substring(int beginIndex,int endIndex)
  • String toLowerCase() 转换为小写
  • String toUpperCase() 转换为大写
  • String trim() 这个字符串将删除原始字符串头部和尾部的空格
  • String join(CharSequence delimiter,CharSequence... elements)

构建字符串

如果单纯用 String 来拼接字符串,每次连接字符串都会构建一个新的 String 对象,既耗时,又浪费空间,使用 StringBuilder 就可以避免这个问题的发生。

//构建一个空的字符串构建器
    StringBuilder builder = new StringBuilder();
        builder.append("Hello");
        builder.append("World");
        String completedString = builder.toString();

StringBuilder 的前身是 StringBufferStringBuffer 的效率略低,但是允许采用多线程的方式执行添加或删除字符的操作,如果所有字符串都再一个单线程中编辑,则应该使用 StringBuilder,这两个类的 API 是相同的。

  • StringBuilder()
  • int length()
  • StringBuilder append(String str)
  • StringBuilder append(char c)
  • StringBuilder insert(int offset,String str)
  • StringBuilder insert(int offset,Char c )
  • StringBuilder delete(int startIndex,int endIndex)
  • String toString()

格式化输出

System.out.printf 沿用了 C 语言库函数中的 printf 方法,可以设置多个参数,例如:

    String name = "Pudge";
        int age = 15;
        System.out.printf("Hello,%s. Next year,you'll be %d", name, age);

每一个以 % 字符开始的格式说明符都用相应的参数替换。个数说明符尾部的转换符将指示被格式化的数值类型:

  • d 十进制整数
  • g 通用浮点数
  • s 字符串
  • c 字符
  • b 布尔
    用于 printf 的标志
  • 给定被格式化的参数索引,例如:%1d

printf 用于输出,可以使用 String.format 方法来创建一个格式化的字符串,而不打印输出。

大数值

如果基本的整数和浮点数精度不能够满足需求,那么可以使用 java.math 包中的两个很有用的类:BigInteger 和 BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger 实现了任意精度的整数运算,BigDecimal 实现了任意精度的浮点数运算。

//使用静态的 valueOf 方法可以将普通的数值转换为大数值
//不能使用+、-、*等运算符,需要使用add、multiply方法
BigInteger a = BigInteger.valueOf(100);
BigInteger c = a.add(b) // c =  a + b 
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); //d = c *(b + 2);

BigInteger API

  • BigInteger add(BigInteger other)//加法
  • BigInteger subtract(BigInteger other)//减法
  • BigInteger multiply(BigInteger other)//乘法
  • BigInteger divide(BigInteger other)//除法
  • BigInteger mod(BigInteger other)//取余
  • int compareTo(BigInteger other)//比较,相等则返回 0,大于则返回 1,小于则返回 -1

BigDecimal API

  • BigDecimal add(BigDecimal other)
  • BigDecimal subtract(BigDecimal other)
  • BigDecimal multiply(BigDecimal other)
  • BigDecimal divide(BigDecimal other , RoundingMode mode)//需要给出舍入方式,如 RoundingMode.HALF_UP 是在学校中学习的四舍五入方式
  • int compareTo(BigDecimal other)
  • static BigDecimal valueOf(long x)
  • static BigDecimal valueOf(long x, int scale)// x / 10^scale

源码中介绍的舍入模式

// Rounding Modes

    /**
     * Rounding mode to round away from zero.  Always increments the
     * digit prior to a nonzero discarded fraction.  Note that this rounding
     * mode never decreases the magnitude of the calculated value.
     */
    public final static int ROUND_UP =           0;

    /**
     * Rounding mode to round towards zero.  Never increments the digit
     * prior to a discarded fraction (i.e., truncates).  Note that this
     * rounding mode never increases the magnitude of the calculated value.
     */
    public final static int ROUND_DOWN =         1;

    /**
     * Rounding mode to round towards positive infinity.  If the
     * {@code BigDecimal} is positive, behaves as for
     * {@code ROUND_UP}; if negative, behaves as for
     * {@code ROUND_DOWN}.  Note that this rounding mode never
     * decreases the calculated value.
     */
    public final static int ROUND_CEILING =      2;

    /**
     * Rounding mode to round towards negative infinity.  If the
     * {@code BigDecimal} is positive, behave as for
     * {@code ROUND_DOWN}; if negative, behave as for
     * {@code ROUND_UP}.  Note that this rounding mode never
     * increases the calculated value.
     */
    public final static int ROUND_FLOOR =        3;

    /**
     * Rounding mode to round towards {@literal "nearest neighbor"}
     * unless both neighbors are equidistant, in which case round up.
     * Behaves as for {@code ROUND_UP} if the discarded fraction is
     * &ge; 0.5; otherwise, behaves as for {@code ROUND_DOWN}.  Note
     * that this is the rounding mode that most of us were taught in
     * grade school.
     */
    public final static int ROUND_HALF_UP =      4;

    /**
     * Rounding mode to round towards {@literal "nearest neighbor"}
     * unless both neighbors are equidistant, in which case round
     * down.  Behaves as for {@code ROUND_UP} if the discarded
     * fraction is {@literal >} 0.5; otherwise, behaves as for
     * {@code ROUND_DOWN}.
     */
    public final static int ROUND_HALF_DOWN =    5;

    /**
     * Rounding mode to round towards the {@literal "nearest neighbor"}
     * unless both neighbors are equidistant, in which case, round
     * towards the even neighbor.  Behaves as for
     * {@code ROUND_HALF_UP} if the digit to the left of the
     * discarded fraction is odd; behaves as for
     * {@code ROUND_HALF_DOWN} if it's even.  Note that this is the
     * rounding mode that minimizes cumulative error when applied
     * repeatedly over a sequence of calculations.
     */
    public final static int ROUND_HALF_EVEN =    6;

    /**
     * Rounding mode to assert that the requested operation has an exact
     * result, hence no rounding is necessary.  If this rounding mode is
     * specified on an operation that yields an inexact result, an
     * {@code ArithmeticException} is thrown.
     */
    public final static int ROUND_UNNECESSARY =  7;
ROUND_UP
向远离零的方向舍入。舍弃非零部分,并将非零舍弃部分相邻的一位数字加一。
ROUND_DOWN
向接近零的方向舍入。舍弃非零部分,同时不会非零舍弃部分相邻的一位数字加一,采取截取行为。
ROUND_CEILING
向正无穷的方向舍入。如果为正数,舍入结果同ROUND_UP一致;如果为负数,舍入结果同ROUND_DOWN一致。注意:此模式不会减少数值大小。
ROUND_FLOOR
向负无穷的方向舍入。如果为正数,舍入结果同ROUND_DOWN一致;如果为负数,舍入结果同ROUND_UP一致。注意:此模式不会增加数值大小。
ROUND_HALF_UP
向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分>= 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。这种模式也就是我们常说的我们的“四舍五入”。
ROUND_HALF_DOWN
向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则为向下舍入的舍入模式。如果舍弃部分> 0.5,则舍入行为与ROUND_UP相同;否则舍入行为与ROUND_DOWN相同。这种模式也就是我们常说的我们的“五舍六入”。
ROUND_HALF_EVEN
向“最接近”的数字舍入,如果与两个相邻数字的距离相等,则相邻的偶数舍入。如果舍弃部分左边的数字奇数,则舍入行为与 ROUND_HALF_UP 相同;如果为偶数,则舍入行为与 ROUND_HALF_DOWN 相同。注意:在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况,如果前一位为奇数,则入位,否则舍去。
ROUND_UNNECESSARY
断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

第四章对象与类

面向对象程序设计概述

面向对象程序设计,简称OOP。Java 是完全面向对象的,必须熟悉 OOP 才能够编写 Java 程序。

class 是构造对象的模板或蓝图,由类构造 (construct) 对象的过程称为创建类的实例 (instance)。
封装 (encapsulation,有时称为数据隐藏),对象中的数据称为实例域 (instance field),操作数据的过程称为方法 (method)。对于每个特定的类实例都有一组特定的实例域值。这些值的集合就是这个对象的当前状态 (state)。实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。
OOP的另一个原则就是可以通过扩展一个类来建立另外一个新的类,在 Java 中,所有的类都源自于 Object。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)。

对象

对象的三个主要特性

  • 对象的行为 (behavior):可以对对象施加哪些操作,或可以对对象施加哪些方法?
  • 对象的状态 (state):当施加那些方法时,对象如何响应?
  • 对象标识 (identity):如何辨别具有相同行为与状态的不同对象?

识别类

识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

类之间的关系

  • 依赖:uses-a 一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类
  • 聚合:has-a 一个类的对象包含另一个类的对象
  • 继承:is-a 一个类是另一个类的子类或者父类,类之间进行了扩展。

使用预定义类

Java 中没有类就不能做任何事情,然而,并不是所有的类都具有面向对象特征。例如 Math 类,在程序中,可以使用 Math 类的方法,只需要知道方法名和参数,而不必了解它的具体实现过程,这正是封装的关键所在,但是 Math 类只封装了功能,它不需要也不必隐藏数据,由于没有数据,因此也不必担心生成对象以及初始化实例域。

对象与对象变量

要想使用对象,必须首先构造对象,并指定其初始状态。然后,对对象应用方法。
在 Java 中,使用构造器 (constructor) 构造新实例,构造器是一种特殊的方法,用来构造并初始化对象。构造器的名字应该与类名相同,要想构造一个类的对象,需要在构造器前面加上 new 操作符。
new Date() System.out.println(new Date()); String s = new Date().toString();
在上述例子种,构造的对象仅使用了一次,如果希望多次使用,需要将对象存放在一个变量中。
Date birthday = new Date();
一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。

Java 类库中的 LocalDate 类

Date 类用来表示时间点,LocalDate 用来表示大家熟悉的日历表达法。
LocalDate.now()// 2017-07-08
还可以调用 LocalDate.of(1999,12,6) 方法来构造对应一个特定日期的对象。
一旦有了一个 LocalDate 对象,可以使用方法 geetYeargetMonthValuegetDayOfMonth得到年、月、日。下列的方法可以得到未来的日期或者过去的日期。

LocalDate c = LocalDate.now();//c 是当前时间
System.out.println(c.plusDays(1));//明天
System.out.println(c.plusDays(-1));//昨天

更改器方法与访问器方法

Java 库的一个较早版本曾经有另一个类来处理日历,名为 GregorianCalendarc.plusDays(1) 不同,GregorianCalendar.add 方法与 plusDays 方法功能差不多,但是是一个更改器方法 (mutator method)。调用这个方法后,GregorianCalendar 对象的状态会改变。

GregorianCalendar c = new GregorianCalendar(1999,1,10);
c.add(Calendar.YEAR, 1);
int year = c.get(Calendar.YEAR);
System.out.println(year);

相反,只访问对象而不修改对象的方法有时称为访问器方法 (accessor method)。例如 LocalDate.getGregorianCalendar.get
下面的代码可以构建一个当月的日历。

//带 * 号表示今天
Mon Tue Wed Thu Fri Sat Sun
                      1   2 
  3   4   5   6   7   8*   9 
 10  11  12  13  14  15  16 
 17  18  19  20  21  22  23 
 24  25  26  27  28  29  30 
 31 
 -----------------------------------------
LocalDate date = LocalDate.now();

    int month = date.getMonthValue();// 7
    int today = date.getDayOfMonth();// 8

    date = date.minusDays(today - 1);// 返回到月初
    DayOfWeek weekday = date.getDayOfWeek();// 得到星期几
    int value = weekday.getValue();//得到月初的星期
    System.out.println("Mon Tue Wed Thu Fri Sat Sun");//先打印好星期行
    for (int i = 1; i < value; i++) {//控制 1 出现的位置
      System.out.print("    ");
    }
    while (date.getMonthValue() == month) {//

        System.out.printf("%3d", date.getDayOfMonth());//打印1

      if (date.getDayOfMonth() == today) {
        System.out.print("*");
      } else {
        System.out.print(" ");
      }
      date = date.plusDays(1);
      if (date.getDayOfWeek().getValue() == 1) {
        System.out.println();
      }
    }

LocalDate API

  • LocalTime now()
  • LocalTime of(int year, int month, int day)
  • int getYear()
  • int getMonthValue()
  • int getDayOfMonth()
  • DayOfWeek getDayOfWeek
  • LocalDate plusDays(int n)
  • LocalDate minusDays(int n)

用户自定义类

要想创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有 main 方法。

Employee 类

class ClassName
{
    field;
    field;
    ...
    constructor1
    constructor2
    ...
    method1
    method2
    ...
}
public class Demo01 {

  public static void main(String[] args) throws Exception {
    Employee[] staff = new Employee[3];
    staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
    staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

    for (Employee e : staff) {
      e.raiseSalary(5);
    }

    for(Employee e :staff){
      System.out.println("name=" +e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
    }
  }
}

class Employee {

  private String name;
  private double salary;
  private LocalDate hireDay;

  //constructor
  public Employee(String n, double s, int year, int month, int day) {
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
  }

  // a method
  public String getName() {
    return name;
  }

  public double getSalary() {
    return salary;
  }

  public LocalDate getHireDay() {
    return hireDay;
  }

  public void raiseSalary(double byPercent) {
    double raise = salary * byPercent / 100;
    salary += raise;
  }
}

当我们的 .java 文件包含 2 个类的时候,我们编译时可以采用这两种方法

javac Employee*.java  //可以使用通配符,所有与通配符匹配的源文件都将被编译成类文件
javac Demo01.java //Java编译器会自动搜索使用的Employee类,并编译

从构造器开始

public Employee(String n, double s, int year, int month, int day){
    name = n ;
    salary = s;
    LocalDate hireDay = LocalDate.of(year,month,day);
}
构造器与类同名,构造器与其他的方法有一个重要的不同。构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用 constructor 来达到重新设置实例域的目的,例如
`james.Employee("James Bond",250000,1950,1,1)`//ERROR,会产生编译错误。

构造器的基本特点:

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有 0 个、1 个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着 new 操作一起调用

隐式参数与显式参数

public void raiseSalary(double byPercent){
    double raise = salary * byPercent / 100 
    salary +=raise;
    //salary的前面省略了参数,
    //完整的写法是
    double raise =  number007.salary * byPercent / 100
    number007.salary +=raise;
}

raiseSalary 有两个参数,第一个参数称为隐式参数 (implicit) 参数,是出现在方法名前的 Employee 类对象。第二个参数位于方法名后面括号中的数值,这是一个显式 (explicit) 参数。隐式参数也被称为方法调用的目标或接收者。
在每个方法中,关键字 this 表示隐式参数。如果需要的话,可以用下列方式编写 raiseSalary 方法:

public void raiseSalary(double Percent){
    double raise = this.salary * byPercent / 100;
    salary +=raise;
}

encapsulation 的优点

使用 public get 方法来代替 public name,将全局变量设置为 private 可以防止受到外界的破坏。
注意:不要编写返回引用可变对象的 get 方法,因为它本身是可变的话,会破坏 encapsulation 性,我们应该只能通过 set 方法来改变对象的状态,如果必须要这样做,那么我们可以使用 clone

class Employee{
    public Date getHireDay()
    {
        return (Date) hireDay.clone();
    }
}

基于类的访问权限

public boolean equals(Employee other) {
    return name.equals(other.name);
  }

这段代码中 Employee 类的 name 变量是 private 修饰的,other.name 意味着我们访问了另一个对象的 private 属性,这与我们之前说的是对不起来的,其原因是: other 是 Employee 类对象,而 Employee 类的方法可以访问 Employee 类的任何一个对象的私有域。

私有方法

有时,可能希望将一个计算代码划分为若干个独立的赋值方法。通常,这些辅助方法不应该成为公有接口的一部分,最好将这样的方法设计为 private。

final

构造对象时必须初始化这样的域,也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。如果将 final 用在一个可变对象上,那么 final 只表示该变量的对象引用不会更改,对象本身是可以更改的。

静态域与静态方法 static

静态域

如果将域定义为 static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。static 修饰的属性属于类,而不属于任何独立的对象。

静态常量

静态变量用得比较少,但静态常量却使用得笔记多。例如 Math.PI

public class Math{
    public static final double PI = 3.14159265358979323846;
}

这样做的好处就是,我们可以不需要构建 Math 的对象,直接通过 Math.PI 来进行访问,同时,设置为 fanal,避免了被修改的问题。

静态方法

静态方法是一种不能向对象实施操作的方法,例如,Math 类的 pow 方法就是一个静态方法。
Math.pow(x,a),在运算时,不使用任何 Math 对象。换句话说,没有隐式的参数。
可以认为静态方法是没有 this 参数的方法,这也说明了为什么静态方法无法调用非静态变量。
在下面两种情况下使用静态方法:

  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供,例如:Math.pow
  • 一个方法只需要访问类的静态域,例如:Employee.getNextId

工厂方法

静态方法还有另外一种常见的用途。类似 LocalDate 和 NumberFormat 的类使用静态工程方法 (factory method) 来构造对象。

 NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
    NumberFormat percentFormatter = NumberFormat.getPercentInstance();
    double x = 0.1;
    System.out.println(currencyFormatter.format(x));  // ¥0.10
    System.out.println(percentFormatter.format(x));   //  10%

为什么 NumberFormat 不利用 constructor 来完成这些操作呢?

  • 无法命名构造器,构造器的名字必须要和类名一样,但是,这里希望将得到的货币实例和百分比实例采用不同的名字。
  • 当使用构造器时,无法改变所构造的对象类型,而 Factory 方法将返回一个 DecimalFormat 类对象,这是 NumberFormat 的子类。

main 方法

需要注意,不需要使用对象调用静态方法。例如,不需要构造 Math 类对象就可以调用 Math.pow。每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。例如,可以在 Employee 类中添加一个 main 方法,如果想要独立地测试 Employee 类,只需要执行 java Employee,如果 Employee 类是一个更大型应用程序的一部分,就可以使用下面这条语句运行程序 java Application,Employee 类的 main 方法永远不会执行。

方法参数

按值调用 (call by value) 表示方法接收的是调用者提供的值。而按引用调用 (call by reference) 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。call by 是一个标准的计算机科学术语。Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

//假定一个方法:试图将参数值增加至3倍
public static void tripleValue(double x)
{
    x = 3 * x ;
}
double percent = 10;
tripleValue(percent)

不过,并没有实现把参数增加到 3 倍。让我们一步步来细化:

  • x 被初始化为 percent 值的一个拷贝(也就是10)
  • x 被乘以 3 后等于 30,但是 percent 还是 10
  • 这个方法结束之后,参数变量 x 不再使用
    然后,方法参数共有两种类型:
  • 基本数据类型 (数字、布尔值)
  • 对象引用
    下面这段代码实现了当对象引用作为参数的时候,方法修改了参数。
public static void tripleDSalary(Employee x){
    x.raiseSalary(200);
}
harry  = new Employee(...);
tripleSalary(harry)

具体的执行过程是:

  • x 被初始化为 harry 值的拷贝,这是一个对象的引用
  • raiseSalary 方法应用于这个对象引用。x 和 harry 同时引用的哪个 Employee 对象的 salary 提高了 200%
  • 方法结束后,参数变量 x 不再使用,当然,对象变量 harry 继续引用那个薪金增至 3 倍的雇员对象。
    下面总结一下 Java 中方法参数的使用情况:
  • 一个方法不能修改一个基本数据类型的参数 (即数值型或布尔型)
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象

对象构造

由于对象构造非常重要,所以 Java 提供了多种编写构造器的机制。

重载

有些类有多个 constructor,例如,可以如下构造一个空的 StringBuilder 对象。
StringBuilder messages = new StringBuilder();
或者,可以指定一个初始字符串:
StringBuilder todoList = new StringBuilder("To do:\n");
这种特征叫做重载 (overloading)。如果多个方法有相同的名字、不同的参数,便产生了 overload。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。要注意的是,重载与返回值无关,也就是说,方法名相同,参数相同,返回值类型不相同是无法构成 overload 的。

默认域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值;数值为 0、布尔值为 false、对象引用为 null。

这是全局变量与局部变量的主要不同点。必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的全局变量,将会被自动初始化为默认值 (0、false或null)。

无参数的构造器

只有当类没有提供任何构造器时,系统才会提供一个默认的无参构造器。

显式域初始化

class Employee{
    private String name  = "";
}

在执行构造器之前,会先执行显式赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方法特别有用。

参数名

在编写很小的构造器时,常常在参数命名上出现错误。
通常,参数用单个字符命名,但是我们推荐这样做:

public Employee(String aName, double aSalary){
    name = aName;
    salary = aSalary;
}

public Employee(String name, double salary){
    this.name = name;
    this.salary = salary;
}

调用另一个构造器

关键字 this 引用方法的隐式参数。然而,这个关键字还有另外一个含义。
如果构造器的第一个语句形如 this(...),这个构造器将调用同一个类的另一个构造器。下面是个典型的例子:

public Employee(double s){
    //call Employee(String, double)
    this("Employee #" + nextId,s);
    nextId++:
}

初始化块

前面已经讲过两种初始化数据域的方法:

  • 在构造器中设置值
  • 在声明中赋值
    实际上,Java 还有第三种机制,称为初始化块 (initialization block)。在一个类的声明种,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如,
class Employee
{
    private static int nextId;
    
    private int id;
    private String name;
    private double salary;
    
    {
        d = nextId;
        nextId++:
    }
    
    public Employee(String n, double s)
    {
        name = n ;
        salary = s;
    }
    
    public Employee()
    {
        name = "";
        salary = 0;
    }
}

通常会直接将初始化代码放在构造器中。
由于初始化数据域有多种途径,所以列出构造过程的所有路径可能相当混乱,下面是具体处理步骤:

  • 所有数据域被初始化为默认值 (0、false 或 null )。
  • 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
  • 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
  • 执行这个构造器的主体。
    还可以使用静态代码块,在类第一次加载的时候,将会进行静态域的初始化。

Random API

  • Random() //构造一个新的随机数生成器
  • int nextInt(int n)//返回一个 0~ n-1 之间的随机数

Java 允许使用 (package) 将类组织起来,借助于包可以方便地组织自己的代码。使用包的主要原因是确保类名的唯一性。

类的导入

一个类可以使用所属包中的所有类,以及其他包中的公有类 (public class),我们可以采用两种方式访问另一个包中的共有类。第一种方式是在每个类名之前添加完整的包名。

java.time.LocalDate today = java.time.LocalDate.now();

这样比较麻烦,我们推荐使用 import 导包,import 语句应该位于源文件的顶部 (但位于 package 语句的后面)。例如,可以使用下面这条语句导入 java.util 包种所有的类。
import java.util.*;,也可以导入特定的类 import java.time.LocalDate;

静态导入

import 不仅可以导入类,还增加导入静态方法和静态域的功能。
例如:import static java.lang.System.*;
我们就可以使用 System 类的静态方法和静态域,而不必加类名前缀:

out.println(&quot;Goodbye,World!&quot;);
//还可以导入特定的方法或域
import static java.lang.System.out;

将类放入包中

要想将一个类放入包中,必须将包的名字放在源文件的开头,包中定义类的代码之前。

package com.horstmann.corejava;

public class Employee{
}

包作用域

标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用。如果没有指定,则为 default,表示可以被同一个包中的所有方法访问。

文档注释

JDK 包含一个很有用的工具,叫做 javadoc。它可以由源文件生成一个 HTML 文档。如果在源代码种 添加以专用的 /** 开始注释,那么可以很容易地生成一个看上去具有专业水准的文档。

注释的插入

javadoc 从下面几个特性种抽取信息:

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 公有的和受保护的域
    每个 /** */文档注释在标记之后紧跟着自由格式文档。标记由 @ 开始,如 @author 或 @param。在自由格式文本种,可以使用 HTML 修饰符,例如:<em> </em><strong> </strong>

类注释

类注释必须放在 import 语句之后,类定义之前。

package com.example;

/**
 * Afdfsdfsdfsdf
 * sdfsdfsdfsdfs
 * fsdfdssfsdfsf
 */
public class PackageTest {

方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:

  • @param 变量描述
  • @return 描述
  • @throws 类描述
    下面是一个方法注释的实例:
/**
   * 我用来说明方法的概要
   * @param s 我用来说明参数的作用
   * @param g 我用来说明参数的作用
   * @return 我用来说明返回值的作用
   */
  public int gogogo(String s , int g ){
    return 4;
  }

域注释

只需要对公有域 (通常指的是静态常量) 建立文档。例如

  /**
   * 我用来说明常量作用
   */
  public static final int YEAR = 5;
  

通用注释

下面的标记可以用在类文档的注释中。

  • @author 姓名,将产生一个 "author" 条目,可以使用多个。
  • @version 版本,这个标记将产生一个 "version" 条目
  • @since 这个标记将产生一个 "since" 条目,这里的 text 可以是对引入特性的版本描述。
  • @deprecated 这个标记将对类、方法或变量添加一个不再使用的注释。
  • @see 引用,它可以用于类中,也可以用于方法中。它有三种情况
    • package.class#feature label
    • <a href="...">label</a>
    • "text"
      @see 的第一种情况最常见。只要提供类、方法或变量的名字,javadoc 就在文档中插入一个超链接。例如:
      @see com.horstmann.corejava.Employee#raiseSalary(double)
      @see com.example.GoGoGo#fuck(String)
      @see GoGoGo#fuck(String) 也可以省略包名。
      如果 @see 标记后面有一个 < 字符,就需要指定一个超链接,可以超链接到任何 URL。
@see <a href ="wwww.baidu.com">百度</a>
@see "百度"

如果愿意的话,还可以在注释中的任何位置放置指向其他类或方法的超级链接,以及插入一个专用的标记,例如:
{@link GoGoGo#fuck(String) label}

包与概述注解

类、方法、变量的注释都可以放置在 Java 源文件中,但是要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:

  • 提供一个以 package.html 命名的 HTML 文件。在标记 <body>..</body> 之间的所有文本都会被抽取出来。
  • 提供一个以 package-info.java 命名的 Java 文件。这个文件必须包含一个初始的以 /** */界定的 Javadoc 注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。
    还可以为所有的源文件提供一个概述性的注释,这个注释将被放置在一个名为 overview.html 的文件中,这个文件位于包含所有源文件的父目录中。标记 <body>..</body> 之间的所有文件将被抽取出来。当用户从导航栏中选择“Overview”时,就会显示出这些注释内容。

类设计技巧

  • 一定要保证数据私有:将全局变量设置为 private。
  • 一定要对数据初始化
  • 不要在类中使用过多的基本类型
  • 不是所有的变量都需要 getset 方法
  • 将职责过多的类进行分解
  • 类名和方法名要能够体现它们的职责
  • 优先使用不可变的类

第六章接口、lambda表达式与内部类

接口

接口概念

在 Java 中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
例如:Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了 Comparable 接口。

//这是 Comparable 的代码
public interface Comparable<T>
{
    int compareTo(T other);
}

接口中的所有方法自动地属于 public。因此,在接口中声明方法时,不必提供关键字 public。接口可以定义常量,接口绝不能含有实例域。

  • Comparable<T>
    • int compareTo(T other):建议实现用这个对象与 other 进行比较。如果这个对象小于 other 则返回负值;如果相等则返回0;否则返回正值。
  • Arrays
    • static void sort(Object[] a):使用 mergesort 算法对数组 a 中的元素进行排序。要求数组中的元素必须属于实现了 Comparable 接口的类,并且元素之间必须是可比较的。
  • Integer & Double
    • static int compare(int x, int y)
    • static int compare(double x, double y)
    • x < y 返回 -1,x=y 返回 0,x>y 返回 1

接口的特性

接口不是类,尤其不能使用 new 实例化一个接口。
然而,尽管不能构造接口的对象,却能声明接口的变量:Comparable x;
接口变量必须引用了实现接口的类对象:x = new Employee()
接下来,可以使用 instanceof 检测一个对象是否实现了某个特定的接口
if(anObject instanceof Comparable)
接口可以实现继承,而且可以多继承。
虽然在接口中不能包含实例域或静态方法,却可以包含常量,接口中的域将被自动设为 public static final
尽管每个类只能拥有一个父类,但却可以实现多个接口。

接口与抽象类

接口与抽象类的目的差不多,都想让实现类实现自己的抽象方法,但 Java 是单继承的,如果没有接口,只有抽象类,那一个类继承完抽象类后,就不能再继承其他类了,所以接口显得更加灵活。

静态方法

在 Java8 中,允许在接口中添加静态方法。

默认方法

可以为接口方法提供一个默认实现。必须用 default 修饰符标记这个方法。不过一般没有什么作用,因为实现了接口的类往往会重新实现这个方法,如果设置了 default,那么在实现接口的时候就不会强制要求你实现这个抽象方法了。

 default int compareTo(Object other) {
    return 0;
  }

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在父类或另一个接口中定义了同样的方法,会发生什么情况?

  • 父类优先。如果父类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
  • 接口冲突。如果一个父接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须 override 这个方法来解决冲突。
//两个接口,同样的方法,一个设置为 default 
public interface Named {
  default String getName() {
    return “d“;
  }
}

interface Person {
  String getName();
}

//当你同时实现这两个接口的时候,编译器会强制要求你实现一个 getName() 方法
//而不是直接使用 Named 的 default 方法
class Student implements Named,Person{

}

那么父类和接口拥有同样的方法会发生什么呢?

public interface Named {
  default String getName() {
    return ”d“;
  }
}


public class Student extends Person implements Named {

}

class Person {
  public String getName() {
    return ”superClass“;
  }
}

此时的话,是“类优先”的,无论 Named 的方法加不加 default,父类的方法都会 override 接口的方法。

接口实例

接口与回调

回调 (callback) 是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。

Comparator 接口

之前我们已经了解了如何对一个对象数组排序,前提是这些对象是实现了 Comparator 接口的类的实例。例如,可以对一个字符串数组排序,因为 String 类实现了 Comparable&lt;String&gt;,而且 String.compareTo 方法可以对字典顺序比较字符串。

public static void main(String[] args) {
    String[] staff = { "fgfgfgf", "v", "zdgdgdgdgdgdgdg", "adgdgdgfgdgdgdgdgdg", "d", "m", "q" };
    Arrays.sort(staff);
    System.out.println(Arrays.toString(staff));
  }
  //[adgdgdgfgdgdgdgdgdg, d, fgfgfgf, m, q, v, zdgdgdgdgdgdgdg]
  //是用字典顺序规律排序的,无视长度

假设我们现在希望用长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序,如果要实现这种情况,可以选用 Arrays.sort 方法的第二个版本,有一个数组和一个比较器 (comparator) 作为参数,comparator 是实现了 Comparator 接口的类的实例。

public class LengthComparator implements Comparator&lt;String&gt; {
  @Override public int compare(String s, String t1) {
    return s.length() - t1.length();
  }
}
public class EmployeeSortTest {

  public static void main(String[] args) {
    String[] staff = { "fgfgfgf", "v", "zdgdgdgdgdgdgdg", "adgdgdgfgdgdgdgdgdg", "d", "m", "q" };
    Arrays.sort(staff);
    System.out.println(Arrays.toString(staff));
    Arrays.sort(staff, new LengthComparator());
    System.out.println(Arrays.toString(staff));
  }
}

//
[adgdgdgfgdgdgdgdgdg, d, fgfgfgf, m, q, v, zdgdgdgdgdgdgdg]
[d, m, q, v, fgfgfgf, zdgdgdgdgdgdgdg, adgdgdgfgdgdgdgdgdg]

对象克隆

本节会讨论 Cloneable 接口,这个接口指示一个类提供了一个安全的 clone 方法。(克隆并不太常见,可以稍作了解,等真正需要时再深入学习)。先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副本都是同一个对象的引用。这说明,任何一个变量改变都会影响另一个变量。

Employee original = new Employee(&quot;John Public&quot;, 50000);
    Employee copy = original;
    copy.raiseSalary(10);

此时,改变 copy 的状态,就会改变 original 的状态。如果我们希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用 clone 方法。

 Employee original = new Employee(&quot;John Public&quot;, 50000);
    //Employee copy =original.clone();
    //copy.raiseSalary(10);  //此时 original 不会发生改变

不过并没有这么简单。clone 方法是 Object 的一个 protected 方法,如果我们使用从 Object 继承得到的 clone 方法,从 A 克隆出一个 B 的话,它们的域如果都是基本数据类型的话,那么是可以实现互不干涉的,但是假设它们的域中包含引用对象,那么 A 和 B 的引用对象域仍然会存在共享的情况。这种默认的 clone 操作叫做浅拷贝,存在缺陷。
不过,通常子对象都是可变的,必须重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。
对于一个类需要确定:

  • 默认的 clone 方法是否满足要求;
  • 是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;
  • 是否不该使用 Clone。
    如果选择第 1 项或第 2 项,类必须:
  • 实现 Cloneable 接口
  • 重新定义 clone 方法,并指定 public 修饰符。
    Cloneable 接口是 Java 提供的一组标记接口 (tagging interface) 之一。也可以称为记号接口 (marker interface)。
    即使 clone 的浅拷贝用法也能够满足要求,还是需要实现 Cloneable 接口,将 clone 重新定义为 public,再调用 super.clone()。下面是个例子。
class Employee implements Cloneable{
@Override
  public Employee clone() throws CloneNotSupportedException {
    return (Employee) super.clone();
  }
  }

下面来看一个深拷贝的 clone 方法的例子:

  @Override
  public CloneTest clone() throws CloneNotSupportedException {
    CloneTest copy = (CloneTest) super.clone();
    copy.mEmployee = mEmployee.clone();
    return copy;
  }

另外,所欲偶的数组类型都有一个 public 的 clone 方法,而不是 protected。可以用这个方法建立一个新数组,包含原数组所有元素的副本,例如:

int[] luckyNumbers= {2,3,5,7,11,13}
int[] cloned = luckyNumbers.clone();
cloned[5] = 12; //不会改变 luckyNumbers[5] 的数值

lambda 表达式

了解如何使用 lambda 表达式采用一种简洁的语法定义代码块,以及如何编写处理 lambda 表达式的代码。

Why lambda ?

lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。在 Java 中,不能直接传递代码块。必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。lambda 的设计,是为了解决 Java 如何做到函数式编程。

lambda 表达式的语法

(String first, String second) -> first.length() - second.length()
参数,箭头 → 以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 {} 中,并包含显式的 return 语句。例如:

(String first, String second) ->
{
    if(first.length() < second.length()) return -1;
    else if(first.length() > second.length()) return 1;
    else return 0;
}

即使 lambda 表达式没有参数,仍然要提供空括号,就像无参数方法一样

() -> {for (int i=100;i>=0;i--) System.out.println(i);}

如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。例如:

Comparator<String> comp = (first, second) ->first.length() - second.length();

在这里,编译器可以推导出 first 和 second 必然是字符串,因为这个 lambda 表达式将赋给一个字符串比较器。
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号;

ActionListener listener = event -> System.out.println("The time is"+new Date());

无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是由上下文推导得出。
(String first, String second) -> first.length() - second.length(),可以在需要 int 类型结果的上下文中使用。

public static void main(String[] args) {
    String[] planets =
        new String[] { "Mercury", "Venus", "Earth", "Jupiter", "Saturn", "Uranus", "Neptune" };
    System.out.println(Arrays.toString(planets));
    System.out.println("Sorted in dictionary order:");
    Arrays.sort(planets);
    System.out.println(Arrays.toString(planets));
    System.out.println("Sorted by length");
    Arrays.sort(planets, (first, second) -> first.length() - second.length());
    System.out.println(Arrays.toString(planets));
  }

函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口 (functionnal interface)。最好把 lambda 表达式看作是一个函数,而不是一个对象,另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口,这一点让 lambda 表达式很有吸引力。

//这是将 lambda 转换为函数式接口的例子
BiFunction<String, Integer, Boolean> comp = (name, age) -> name.length() > age;

ArrayList 类有一个 removeIf 方法,它的参数就是一个 Predicate。
public interface Predicate<T> {boolean test(T t);},这也是一个函数式接口,所以我们可以使用 lambda。

 ArrayList<String> a = new ArrayList<>();
    a.add(null);
    a.add(null);
    a.add(null);
    a.add(null);
    a.add("dsada");
    a.add("czxzd");
    a.add("gadga");
    a.add("zcbzc");
    a.removeIf(e -> e == null);
    for (int i = 0; i < a.size(); i++) {
      System.out.println(a.get(i)); 
    }
    //Predicate 的泛型是根据 ArrayList 的泛型的,这里的代码就是将 ArrayList 中的 null 值都删除了。

方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
Timer t = new Timer(10000,System.out::println),等价于 x -> System.out.println(x)
再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgonreCase)
从这些例子可以看出,要用 :: 操作符分隔方法名与对象或类名。主要有 3 种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
    在前 2 种情况种,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到,System.out::println 等价于 x -> System.out.println(x)。类似地,Math::pow 等价于 (x,y) ->Math.pow(x,y)
    对于第 3 种情况,第 1 个参数会成为方法的目标。
    例如,String::compareToIgnoreCase 等同于(x,y) ->x.compareToIgnoreCase(y)
    可以在方法引用中使用 this 参数。例如,this::equals 等同于 x-> this.equals(x)。使用 super 也是合法的。下面的方法表达式
    super:instanceMethod

构造器引用

构造器引用与方法引用很类似,只不过方法名为 new。例如,Person::new 是 Person 构造器的一个引用。哪一个构造器呢?这取决于上下文。

public static void main(String[] args) {
    ArrayList<String> names = new ArrayList<>();
    Stream<Person> stream = names.stream().map(Person::new);
  }
}

class Person {
  Person(String name) {

  

重点是 map 方法会为各个列表元素调用 Person(String) 构造器。
可以用数组类型建立构造器引用。例如,int[]::new 是一个构造器引用,它有一个参数:即数组的长度。这等价于 lambda 表达式 x -> new int[x]

@FunctionalInterface interface Fuck {
  int[] createIntegerArray(int length);
}
public static void main(String[] args) {
    Fuck fuck = int[]::new;
  }

Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于客服这个限制很有用。表达式 new T[n] 会产生错误,因为这会改为 new Object[n]。不过 toArray 有一个重载方法,引用一个函数式接口,解决了这个问题。

Person[] people = stream.toArray(Person[]::new);

变量作用域

通常,你可能希望能够在 lambda 表达式中访问外围方法或类中的变量。

public static void repeatMessage(String test, int delay) {
    ActionListener listener = event -> System.out.println(test);//这里只是实现了接口的方法而已,并没有马上调用
    test = "Change";//当我们改变test值的时候,编译器会报错
  }
  Variable used in lambda expression should be final or effectively final

lambda 表达式有 3个部分:

  • 一个代码块
  • 参数
  • 自由变量的值,这是指非参数而且不在代码中定义的变量。
    在我们的代码中,lambda 有 1 个自由变量 test。表示 lambda 的数据结构必须存储自由变量的指。在这里就是字符串 test。我们说它被 lambda 捕获 (captured)。例如,可以把一个 lambda 转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。
    关于代码块以及自由变量值有一个术语:闭包 (closure),在 Java 中,lambda 表达式就是闭包。
    在 lambda 中,只能引用值不会改变的变量,也不能在 lambda 中改变自由变量的值。
    在一个 lambda 中使用 this 关键字时,是指创建这个 lambda 的方法的 this 参数。
public class Application()
{
    public void init()
    {
        ActionListener listener = event ->
            {
                System.out.println(this.toString());
            }
    }
}

这里会调用 Application 对象的 toString 方法,而不是 ActionListener 实例的方法。

处理 lambda 表达式

下面来看如何编写方法处理 lambda 表达式。
使用 lambda 的重点是延迟执行 (deferred execution)。之所以希望以后再执行代码,这有很多原因

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法适当位置运行代码
  • 发生某种情况执行代码,点击了一个按钮,数据到达,等等
  • 只在必要时才运行代码
    可以为你自己设计的函数式接口打上 @FunctionalInterface 标记。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容