目录

目录

1、JAVA概述

语言:人与人交流沟通的表达方式

计算机语言:人与计算机之间进行信息交流沟通的一种特殊语言

Java语言是美国Sun公司(Stanford University Network)在1995年推出的计算机语言

Java之父:詹姆斯·高斯林(James Gosling)

静态语言:

与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。

Java不是动态语言,但Java可以称之为“准动态语言”即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。

Java的动态性让编程的时候更加灵活!

1.1 JAVA语言发展史

1972年 C诞生

贴近硬件,运行极快,效率极高。

操作系统,编译器,数据库,网络系统等

指针和内存管理

1982年 C++诞生

面向对象

兼容C

图形领域、游戏等


我们要建立一个新的语言∶

语法有点像C

没有指针没有内存管理

真正的可移植性,编写一次到处运行

面向对象

类型安全

高质量的类库

Java与C++的区别:(-3+1)去掉指针,去掉多继承,去掉运算符重载;增加自动内存分配与回收机制。

Java是一种面向对象的语言,其三大特性分别是,封装、继承、多态

三高:高可用 高性能 高并发

他们基于Java开发了巨多的平台,系统,工具

构建工具:Ant,Maven,Jekins

应用服务器: Tomcat,Jetty, Jboss, Websphere, weblogic

Web开发: struts,Spring,Hibernate, myBatis

开发工具: Eclipse, Netbean,intellij idea, Jbuilder

2006:Hadoop (大数据)

2008:Android(手机端)

1992

1995年 java语言诞生

  • 1996年 java1.0 221个类
  • 1997年 java1.1 477个类
  • 1998年 java1.2 分为三个版本 SE EE ME
  • 2000年 java1.3
  • 2002年 java1.4
  • 2005年 java(5.0)
  • 2011年 java 7
  • 2014年 Java 8
  • 2017年 java 9

Java的版本分为:

JavaSE:标准版 (桌面程序,控制台开发……)

JavaME:嵌入式开发 (手机,小家电….)

JavaEE : 企业级开发 (web端,服务器开发…)

1.0 Java特性和优势、版本特性

  1. 简单性
  2. 面向对象 java一开始就是面向对象设计的
  3. 可移植性 java虚拟机
  4. 高性能
  5. 分布式
  6. 动态性
  7. 多线程
  8. 安全性
  9. 健壮性

java的优势:

好的语言

执行环境

庞大\丰富的库


Java 9

  1. Applet被废弃
  2. 新增JShell

Java 10

  • 局部变量的类型推断 var关键字

    • 减少与编写Java相关的冗长度,同时保持对静态类型安全性的承诺。

    • 注意赋值语句右边,最好写上泛型类型,否则可以加入list.add(1);

    • public static void main(String[] args) {
            var list = new ArrayList<String>();
            list.add("hello,world!");
            System.out.println(list);
        }
      
    • 注意:下面几点使用限制

      • 局部变量初始化

      • for循环内部索引变量

      • 传统的for循环声明变量

      • public static void main(String[] args) {
                //局部变量初始化
                var list = new ArrayList<String>();
                //for循环内部索引变量
                for (var s : list) {
                    System.out.println(s);
                }
                //传统的for循环声明变量
                for (var i = 0; i < list.size(); i++) {
                    System.out.println(i);
                }
            }
        
    • 下面这几种情况,都是不能使用var的

      • 方法参数

      • 全局变量

      • public static var list = new ArrayList<String>(); //编译报错
        public static List<String> list = new ArrayList<>(); //正常编译通过
        

Java 11

增加了新技巧,来运行一个java文件。如果程序包含在单个文件里那么不需要先编译这个文件。可以直接使用java

java HelloWrold.java
等同于
javac HelloWrold.java
java HelloWrold

1.2 JDK、JRE、JVM

JDK(Java Development Kit)

是Java程序开发工具包,包含JRE和开发人员使用的工具。

我们想要开发一个全新的Java程序,那么必须安装JDK。

开发工具:

编译工具(java.exe)

打包工具(jar.exe)

JRE(Java Runtime Environment)

是Java程序的运行时环境,包含JVM(Java虚拟机)和运行时所需要的核心类库。想要运行一个已有的Java程序,那么只需安装JRE即可。

JVM(JAVA Virtual Machine)

java虚拟机 Write Once、Run Anywhere

Java是运行在虚拟机(JVM)上的 因此可以实现跨平台 Java程序可以在任意操作系统上运行

jvm是一个虚拟的机,具有指令集并使用不用的储存区域.负责执行指令,管理数据\内存\寄存器.

对于不同的平台,有不同的虚拟机.

Java虚拟机,机制屏蔽了底层运行平台的差别,实现了”一次编译,到处运行”.

编译执行过程:

Java代码–>(通过Java.exe编译).class –>(通过Java.exe执行)不同系统上的jvm

平台:指的是操作系统如:Windows Mac Linux

JDK和JRE、JVM的关系:

JDK包含JRE和开发工具

JRE包含JVM和核心内库


Java 8U65

U:表示BUG修正版本

U65表示第65次更新

1.3 JDK的下载and卸载

安装:

通过官方网站获取JDK http://www.oracle.com注意:针对不同操作系统,下载对应的JDK

傻瓜式安装,下一步即可。

建议:安装路径中不要包含中文和空格。所有的开发工具最好安装目录统一。

  1. 记住安装路径
  2. 配置环境变量
    1. JAVA_HOME 变量的值为java安装的位置
    2. path中配置引用
      • %JAVA_HOME%\bin
      • %JAVA_HOME%\jre\bin
  3. cmd命令行验证JDK是否安装成功
    1. java -version

打印出java的版本号


卸载:

  1. 删除java的安装目录
  2. 清理环境变量
    1. 删除JAVA_HOME
    2. 删除path下关于java的目录
    3. cmd 命令行java -version 显示无效命令

Hello world

  • 新建文件

    • 新建Hello.java文件

    • Hello文件内输入

      public class Hello {
          public static void main(String[] args) {
              System.out.println("hello world");
          }
      }	
      
  • java的bin下的

    • javac.exe文件编译XXX.java文件 编译完成后会生成一个class文件
    • Java.exe文件执行XXX.class文件
  • 命令行

    • javac Hello.java
      java hello
      
  • 问题

    • 每个单词的大小不能出现问题,
    • Java是大小写敏感的
    • 尽量使用英文
    • 文件名和类名必须保证一致,并且首字母大写符号使用的了中文

1.4 java的运行机制

编译型 全文翻译 如: C C++

解释型 逐行翻译 如:javaScript HTML

运行机制:

  1. 源文件 *.java文件
  2. java编译器
  3. *.class字节码文件
  4. 类装载器
  5. 字节码校验文件
  6. 解析器–机器码文件
  7. 操作系统平台

java程序是由java虚拟机负责解释运行的,而非操作系统.这样做的好处是可以实现Java程序的跨平台运行

java程序是跨平台的

但java虚拟机不是跨平台的

1.5 开发工具 IDEA

集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器调试器图形用户界面等工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的开发软件服务套。

  • Visual Studio
  • IDEA
  • Eclipse
  • PyCharm

1.6 Java的栈、堆和方法区

栈(Stack)

Java中一个线程一个栈区,每一个栈中的元素都是私有的,不被其他栈所访问。栈有后进先出的特点,栈中的数据大小与生存期都是确定的,缺乏灵活性,但是,存取速度比堆要快,仅次于CPU中的寄存器,另外栈中的数据是共享的

在Java中,所有的基本数据类型和引用变量(对象引用)都在栈中存储,栈中数据的生存空间一般在当前的scopes内,也就是“{}”的部分,比如:函数的参数值,局部变量等,是自动清除的。

局部变量

一个栈对应一个线程

堆(Heap)

内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!

常量池也在堆中

可达性分析算法:

将”GC Roots”对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

一个对象的大小组成

  • 成员变量的字段之和
  • 字段头
  • 字段对齐

方法区

1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

JAVA规则

Java中关键字小写,类名第一个字母大写。

Java是由一个一个类构成的;

示例代码

class HelloWorld{
    //main方法,这是程序的入口
    public static main(String[] arge){
        //程序输出语句
        System.out.println("HelloWorld");
    }
}

注意:

1.模板,类中可以用main方法,格式是固定的[public static main(String[] arge){}],main是程序的入口。方法内是程序的执行部分

2.多个类允许在同一个源文件中,但是,编译出来的,有几个类就会对应生成几个class文件

3.public是个关键字,可以修饰类,如public class 。。。若用来public修饰了一个类,则这个类可以用在别的类里面,而且,文件名必须与这个类名一样,并且一个源文件中,最多只有一个类用public声明

2、基本语法

0.0 mian方法、块

java中会有一定数量的样板代码

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
出于一些原因 main必须声明为 public static 和 void
main 可以从命令行中接收参数:但即使没有 也要声明参数   String[] args 
public 称为访问修饰符    

每个main程序都必须有一个main方法

就像C和C++一样, 在java中 块 用{ }标识,所以有匹配的开始和结束的大括号

在java中语句用分号 ;结束

java是区分大小写的

源代码的文件名必须和公共类的名字相同,并用.java作为扩展名 类名为Hello就必须把它放在一个名为Hello.java的文件中

.用于调用方法


System.out.println("Hello World");
System.out是一个对象
println是一个方法是对System.out做的一件事情	打印括号里的参数内容    

方法调用的一般结构

首先有一个对象如:System.out
然后是一个方法如:println
再然后可以有一些参数如:我们想打印的消息    Hello World

1.0 注释、标识符、关键字

注释:注释并不会被执行,而是给我们写代码的人看的,是代码的备注

注释分单行注释和多行注释

//单行注释
/*
    多行
    注释
*/

javaDoc:文档注释

/**
*@author   指定Java的作者
*@version   指定源文件的版本
*@param     方法的参数说明信息
*/

注释内容可以被JDK当中的javadoc工具所解析,生产一套以网页文件形式体现的该程序的说明文档

如同C、C++一样,注释不能嵌套

关键词

image-20220328151112441

定义:被Java语言赋予了特殊含义,作专门用途的字符串

特点:无大写

用于定义数据类型的关键字:

int class Boolean interface Enumlong void float byte double short char

用于定义数据类型值的关键字:

true false null

用于定义流程控制的关键词:

if else switch case default while do for do break continue return

标识符

定义:Java对各种变量,方法和类等要素命名时使用的字符串序列称为标识符

特点:由自己命名

定义合法标识符的规则:

  1. 字母、数字、_、$

  2. 数字不开头

  3. 不可以使用关键字和保留字,但可以包含关键字和保留字

  4. Java中严格区分大小写,但不限定长度

  5. 标识符不能包含空格

Java命名规范:

  1. 包名:多单词组成时都用小写
  2. 类名\接口名:多单词组成时,所以的单词首字母都大写
  3. 变量名\方法名:多单词组成时,第一个单词首字母小写,第二个单词开始的每个单词首字母都大写
  4. 常量名:所有字母都大写,多单词时用下划线连接
  5. 不建议使用中文或者拼音命名

2.0 数据类型

强类型语言:要求变量的使用要严格符合规定所有变量都必须先定义后才能使用 如:java

弱类型语言:javaScript

Java的数据类型分为两大类:

  • 基本类型(primitive type)

    • 数值类型

      • 浮点类型
      • 整数类型
    • 字符型

    • 布尔型

  • 引用类型(reference type)

    • 接口、类、数组、枚举、注解

在Java中类型要放在变量名前面

数值数据类型

java有4种整数类型:

  • int 4个字节 范围[- 2^31, 2^31 – 1]具体:-2147483648 到 2147483647
  • short 2个字节 范围[- 2^15, 2^15 – 1] -32768 到 32767
  • long 8个字节 范围[- 2^63, 2^63 – 1,默认值为0L] 具体:-9223372036854775808 到 9223372036854775807
  • byte 1个字节 范围[- 2^15, 2^15 – 1]具体-128 到 127 一字节8位

字面量(literal):是用于表达源代码中一个固定值的表示法

Long: 4000000000L					//如果一个数字以L结尾	表示一个Long类型的数
LHex: 0xCAFE						//以OX开头			表示一个十六进制的数
Binary: 0b1111_0100_0010_0100_0000	//以0b开头	表示一个二进制数
数字字面量的下划线都会被忽略	   可以按照自己希望的方式来进行分隔各个分组
!整数的默认类型是int  若一定要使用long类型 需要加上一个后缀L    
举例:System.out.println(100L);     即100是long类型而不是默认的int类型       

java有两种浮点类型:

  • float 4个字节image-20220413121250727
  • double 8个字节

浮点型可能只是一个近似值,并非精确的值.因为二进制无法准确表示分数

浮点数当中默认类型是double。若一定要使用float类型,需要加上一个后缀F(小写也可,推荐大写)


  • cher 原来用来表示Uincode值 现在有的Uincode
'\u2121'  \u开头后面跟一个4位的16进制的数来表示一个任意的Uincode值   \u2121商标符号
char通常为ASkll码    48为'0 '   65为'A'    97为'a'

数字和字符的对照关系表(编码表):
ASCII码:美国信息交换标准代码
Unico码表:万国码。也是数字和符号的对照关系表,开头0-127部分和ASCII完全一样,但是从128开始包含有更多的字符


  • boolean 只有两个值 ture false

说明:

!数据范围与字节数不一定相关。 float数据范围比long更加广泛但是,float是四字节,long是8字节

原因解析

long整型数,在内存中占用8个字节共64位,它表示的数值有2的64次方,平分正负,数值范围是负2的63次方到正2的63次方-1。而float在内存中占4个字节,共32位,但是浮点数在内存中是这样的:

V=(-1)^s * M * 2^E

浮点数的32位不是简单的直接表示大小,而是按照一定的标准分配的。

其中第1位,符号位,即S。

接下来的8位,指数域,即E。

剩下的23位,小数域,即M,M的取值范围为[1,2)或[0,1)。


数据类型转换

自动类型转换:容量小的向容量大的进行转换(隐式)

1.特点:代码不需要进行特殊处理自动完成
2.规则:数据范围从小到大 (与字节数不一定相关)

注意:

1.byte、short只能向int转换。byte不能向short转换。即只要short、byte、char之间做运算,都向int转换

强制类型转换:容量大的向容量小的进行转换

1.特点:代码需要进行特殊的格式处理。不能自动完成

2.格式:范围小的类型 范围小的变量名 = (范围小的类型)原本范围大的数据;

int i= (int)1000L;   //int类型转换为long类型
double j= 9.7;
(int)j=9;		//double转化为int 精度丢失
如果要四舍五入的话使用函数
double  m=9.3;  	
Math.round(j)=10		//默认返回long类型
Math.round(m)=9

注意:

  1. 不推荐使用可能会发生精度损失、数据溢出
  2. byte/short/char这三种类型都可以发生数学运算,例如java char ’a‘+1 a作 ascll码 65
  3. byte/short/char这三种类型在运算的时候,都会被首先提升为int类型再计算。
  4. 字符串与基本类型之间的运算:只能是连接运算”+”,其结果仍为一个字符串.

image-20220330162017072

枚举类型

EnumSize {SMALL,MEDIUM,LARGE,EXTRA_LARGE};//枚举中列出了所有可能的值
//将Size设置为固定的四个值

声明一个这个类型的变量

//Size是类型 	s是变量名
Size s = Size.MEDIUM;	//初始化这个变量
//s 只可能是四个变量中的一个无法被赋予其他值	除了NULL

变量:

程序运行期间,内容可以发生改变的量

在Java中类型要放在变量名前面

数据类型  变量名称     //创建一个变量
int i;

初始化

变量名称 = 数据值      //给变量赋值
i=0;
//在java中可以在你希望的任何地方声明变量

声明变量后可以重新赋一个值

数据类型   变量名称  = 数据值  //创建一个变量并赋值
int i=10;

将数值设置为常量,将不再可修改

final int i = 12;
//常量	在程序运行期间,固定不变的量

【作用域】:从定义的变量的一行开始,一直到所属的大括号结束为止 (局部变量)

注意:

  • !如果创建多个变量,那么多个变量的名称之间不可以重复
  • !对于float和long类型后缀F和L必须有
  • !没有进行赋值的变量不能直接使用。必须赋值后使用
  • !布尔类型不能进行数据类型转换

常量:

在程序运行期间,固定不变的量
1.字符串:用双引号引起来的 可以为多个 “数据” “abf” “123hg1” 可以为空
2.整形常量: 整数 没有小数点,可以为负数 1 2 244 -256
3.浮点数常量:有小数点的数字 12.4 199.0 -12.99
4.字符常量:用单引号引起来的的“单个字符” ‘1’ 不能为空,两个单引号中间必须有且仅有一个字符,可以为空格
5.布尔常量: 只有两种取值。 frue(真)、false(假)
6.空常量:null 代表没有任何数据 空常量不能直接打印输出

局部变量和成员变量

  • 定义的位置不一样。局部变量定义在方法的内部,成员变量定义在方法中。
  • 作用范围不一样(方法域)。局部变量只在方法中才可以使用,成员变量在整个类中都有用。
  • 默认值不一样。局部变量,没有默认值,如果想使用必须手动进行赋值。成员变量,如果没有赋值,会有默认值,规则同数组。
  • 内存的位置不一样。局部变量为于栈内存,成员变量位于堆内存。
  • 生命周期不一样。局部变量,随着方法进栈而诞生,随着方法出栈而消失。成员变量,随着对象创建而诞生,随着被垃圾回收而消失。

3.0 运算符:

运算符:进行特定操作的符号。列如 := , +

表达式:用运算符连接起开的式子叫表达式 例如;

int a,b,c,d = 2;
a = b+c+d;

算数运算符

两个常量之间可以进行数学运算,首先计算表达式的结果,然后再打印输出 变量和常量之间也可以进行运算

对于一个整数表达式,除法用的是整除,整数除以整数,结果仍是整数,只看商,不取余

!一旦运算中有不同的数据类型,那么结果将是数据范围大的那种 比如intlong

  • + java 对于数值来说,就是java

    • 对于char类型,在计算之前,char会被提升为int,然后再计算
    • 对于字符串String来说,加号代表字符串连接操作
    • 任何数据类型和字符串进行连接的时候,结果都会变成字符串
  • - 减法

  • / 除法

  • * 乘法

  • % 取模

在java中计算平方根和幂需要使用Math函数

Math.sqrt(X)	//平方根
Math.pow(a,b) 	//幂				a的b次幂   
Math.floorMod(-15,2)   //取余且结果为正数 

赋值运算符号

代表将右侧的数据交给左侧的变量。

  • = 赋值运算
  • 复合赋值运算符:
    • += a+=4 等同于 a=a+4
    • -= b-=2 相当于 b=b-2
    • /= d/=4 相当于 d=d/4
    • *= c*=3 相当于 c=c*3
    • -- 自增
    • ++ 自减

自增:++ 自减:-- 使变量改变1
当在混合使用时
A.如果是【前++】那么变量【立刻马上加一】,然后拿着结果进行使用 【先加后用】
B.如果是【后++】,那么首先使用变量本来的数值,【然后再让变量加一】 【先用后加】
【!只有变量才能使用自增和自减,否则会报错】

关系运算符

  • == 比较是否相等
  • < 比较左边是否小于右边,成立是则输出 true 真
  • > 比较右边是否大于左边,不成立则输出 false 假
  • <= 小于等于
  • >= 大于等于

boolean运算符

  • && 与 都是true,才是true,否则是false
  • || 或 至少是一个true,就是true;都是false,才是false
  • ! 非 本来是true,变成false。本来是false变成true

【短路】 &&|| 具有短路效果;如果根据左边已经可以判断得到最终结果,那么右边的代码将不再执行,从而节省一定的性能

逻辑运算符只能用于boolean值

&&||需要左右各自有一个Boolean值,但 反只有有唯一的一个Boolean值即可

三元运算符or条件运算符

  • x<y? x:y

格式:数据类型 变量名称 = 条件判断? 表达式A :表达式B
:】 的两边必须为同数据类型 错误表达: a>b? 2.5:6

位运算符

  • 位逻辑运算符

    • &A&B:有假则假
    • |A|B:有真则真
    • ^ 异或 A^B:不等则真
    • ~ 取反 ~A:取反 最高位符号位为 1 就为负数
  • 位移运算符

    • >> 右移位 1>>n:右移n位
    • >>> 无符号移位
    • << 左移位 1<<n:左移n位

将其转化为2进制再计算

因为2进制一位代表 \(2^n\) 所以在没有舍去溢出值的情况下,左移一位乘2,右移一位除2

也就是说如果一个整数正好时 2的n次方 直接计算即可 乘除 \(2^n\)

  • 符号位 第一位为0正数 1 负数
  • 右移
    • 右移的是负数,会在高位补1 保证负数右移后还是负数
    • 右移的是正数,会在高位补0
    • 右移的溢出越界会舍去
  • 左移
    • 高位溢出
    • 地位补0
  • 无符号右移
    • 不会看操作数的正负,高位一律补零
    • byteshort不适合做无符号位移 short 2个字节16位 byte一个字节8位

进制转换:

非位置化数字系统: 罗马数字 ⅢⅢ 两个 表示六

位置化数字系统: 二进制|八进制|十进制|十六进制 十进制 33 两个3代表三十三


十进制

十进制:用10个可用符号来表示一个数字 [0 1 2 3 4 5 6 7 8 9]

\(235 = [2\times10^2] +[3\times10^1]+[5\times10^0]\)

2是\(10^2\) 所以是百位 3是\(10^1\) 所以是十位 5是\(10^0\) 所以是个位

转二进制

连除法:

\(29_{(10)}\div2=14_{(10)}余[1]\div2=7_{(10)}余[0]\div2=3_{(10)}余[1]=1_{(10)}\div2余[1]=1_{(10)}\div2余[1]\)

把余数反向排列: 等同于除2的N次方,最后的除最大

\(29_{(10)}=11101_{(2)}\)

转八进制

\(900_{(10)}\div8余[6] ==1406\)


二进制

二进制:用2个可用符号来表示一个数字 [0 1]

转十进制

\(1011_{(2)}=[1\times2^3]+[0\times2^2]+[1\times2^1]+[1\times2^0]=11_{(10)}\)

第一位1是\(2^3\)所以是八位 第二位0是\(2^2\)所以是四位 第三位1是\(2^1\)所以是二位 第二位1是\(2^1\)所以是个位

转八进制

由于\(2^3=8\),所以每\(3\)位二进制可以转换为\(1\)位八进制

\(10111001_{(2)}=271_{(2)}\)

001=1

111=7

10=2

转十六进制

由于\(2^4=16\),所以每\(4\)位二进制可以转换为\(1\)位十六进制

\(10111001_{(2)}=B9_{(16)}\)

1001=9

1011=B


八进制

八进制:用8个可用符号来表示一个数字 [0 1 2 3 4 5 6 7]

转十进制

\(277_{(8)}=[2\times8^2]+[7\times8^1]+[7\times8^0]=191_{(10)}\)


十六进制

十六进制:用16个可用符号来表示一个数字 [0 1 2 3 4 5 6 7 8 9 A B C D E F] = [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]

转十进制

\(2AE_{(16)}=[2\times16^2]+[10\times16^1]+[14\times16^0]=686_{(10)}\)


注意!

  1. 一元运算符:只需要一个数据就可以进行操作的运算符。 例如:取反! 自增++ 自减–

  2. 二元运算符:需要两个数据才能进行操作的运算符,例如: java+ 赋值=

  3. 三元运算符:需要三个数据才能进行操作的运算符 例如:int id = a>b ? a:b a大取a a小或等取b

原码、反码、补码

计算机里都是以补码的形式存储数据!

计算机没有减法器只有加法器

如何处理减法 5-5=? 转换成5+(-5)

计算机用补码进行计算

计算:\(5-5\)

\(+5=0000\_0101_{反码}\)

\(-5=1111\_1010_{反码}\)

\(5-5=1111\_1111_{反码}=1000\_0000_{原码}=-0\)

-0和0是值一样,但二进制不一样不能作为结果

\(+5=0000\_0101_{反码}\)

\(-5=1111\_1011_{补码}\)

\(5-5=1\_0000\_0000_{反码}=0000\_0000_{原码}=0\) 高位舍去


原码

原码:一个字节(8位)为例

最高位为符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制。

原码=最高位(符号位)+ 低位(数值位)

反码

正数的反码与原码一致

负数的反码是对原码按位取反,只是最高位(符号位)不变。

补码

正数的补码与原码一致;

负数的补码是该数的反码加1。

\(10001_{反码}=10010_{补码}\)

4.0 字符串

Java中的字符串是一个Unicode字符序列

字符串以双引号""包括

  • 字符串的位置从0开始
Stringgreeting = "Hello" ;
Strings = greeting. substring(0,3);	//返回一个子串,从下标0开始,长度为3
  • 字符串拼接

使用+

Stringexpletive = "Expletive" ;
StringPG13 = "deleted" ;
Stringmessage = expletive + PG13;
//打印	message = Expletivedeleted

如果拼接的另一个数不是字符串,会自动将其转换为字符串

int age = 13;
Stringrating ="PG" + age;
//打印	Pg13

如果需要将某个值转换为字符串,只需要将其与空串相加即可

""+12
//打印    	
  • java的字符串是不可变的,永远无法修改一个字符串

但可以将其赋一个新的值

Stringgreeting = "Hello" ;
gerrting="world"			//改变了变量的内容,但原来的字符串"Hello"仍然是“Hello"它只是不再存储在这个变量中

比较字符串

"Hello".equals(greeting)				//若两者相等返回true,否则返回false
"Hello".equalsIgnulleCase(greeting)		//和equals一致且忽略大小写
  • 空串""即长度为0的字符串,与null是不同的
    • Strings="" s.length() 长度为0
    • null可以放在一个String变量中,指示没有任何字符串,连空串都没有
    • 如果指示Strings=null; s.length()程序会报指针异常Nul IPointerException停止运行
  • 在java中==的含义:它们是不是相同的对象,即是不是内存中的同一个对象? 对于字符串往往不是这样
    • 如果将字面量hel和自身比较,这就是同一个字符串
    • 如果与一个计算得到的字符串比较,==就毫无意义
  • 不要使用==符号来比较字符串
    • 这是因为==会检查两个字符串,是否是内存中的同一个字符串
    • 一些方法返回字符型是建立一个新的地址来储存字符串

举例:

"Hello".substring(0,3)=="Hel"	//false
//"hel"					是一个字面量字符串                
//"Hello".substring(0,3)是一个计算得到的字符串,会分配新的字符串来保存这个结果,而它会放在内存中一个不同的位置上
//所以这两个字符串当然是不相同的              

字符串长度

  • 对于一个字符串可以调用length()方法计算它的长度
  • length()返回的是cher值的个数,这与Unicode字符个数,可能相同也可能不同
    • 即一个字符可能占用两个空间
    • 某些符号需要两个char值来编码

  • charAt()方法会提供第 \(i\) 个char值

    • Strings = "Hello" ;
      s.charAt(0)	 	// H
      

String类的其他方法

  • trim()方法会剔除空格
    • 开头和末尾的空格都已经去除
    • 但中间的空格仍保持不动
Stringa =" 南 阳 ";
Stringtrim = a.trim();
System.out.println("trim = " +"("+ trim +")");
System.out.println("trim.length() = " + trim.length());
System.out.println("a = " +"("+ a +")");
System.out.println("a.length() = " + a.length());
//打印
    trim = (南 阳)
    trim.length() = 3
    a = ( 南 阳 )
    a.length() = 5
  • toLowerCase()方法,用于将给定的字符串转换为小写。
    • 该方法不更改字符串。 它返回小写转换后的新字符串。
Stringstr = "Hello World!";
Stringans = str.toLowerCase();
System.out.println("ans = " + ans);

Stringstr2 = "Hello".toLowerCase();
System.out.println("str2 = " + str2);
//打印
ans = hello world!
str2 = hello
  • int indexOf(Stringstr): 返回指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1
  • int indexOf(Stringstr, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。
  • lastIndexOf(int ch) 说明:返回指定字符在此字符串中最后一次出现处的索引。 返回:int

没有方法能够修改字符串,Java中的字符串是不可变的

5.0输入、输出

输入(Scanner)

可以实现键盘输入数据到程序当中。

Scanner类在一个名为java. utiI的包中定义,使用Scanner类要把这个import语句放在文件最上面

引用类型的一般使用步骤;

A。导包 impact 包路径.类名称;只有Java.lang包下的内容不需要导包。 导包需要在packge后,类前。
B。创建 类名称 对象名 = new 类名称();
C。使用。 对象名.成员方法名()

示例代码

Scanner scanner = new Scanner(system.in);	//如何创建一个Scanner对象
//system.in 表示预定义的控制台对象	这个对象本身并不能读取数字和字符串,所以在这里我们把它变成一个Scanner
//调用new Scanner从System.in,构造一个新的Scanner

任何时候想要处理输入,只需要复制这行代码,就会得到Scanner

四个方法

  • next() 获取输入的字符串
    • 一定要读取到有效字符后才可以结束输入。
    • 对输入有效字符之前遇到的空格,Tab键或Enter键等结束符,next()方法会自动将其去掉
    • 只有在输入有效字符之后,next()方法才将其后输入的空格键、Tab键或Enter键等视为分隔符或结束符
    • 以一个字符串返回
  • nextLine() 获取输入的字符串
    • 以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符
    • 是可以得到带空格的字符串的
    • 以一个字符串返回
  • nextInt() 得到下一个整数,作为一个int返回
  • nextDouble()得到下一个双精度数,作为一个double返回

  • 从文件读取输入

  • Scanner in = new Scanner(Paths.get ("myfile.txt"),"UTF-8");
    
    • 获取从文件读取输入的Scanner,现在要使用这个构造
    • Scanner不是由System. in构造,而是由一个Path构造的,需要提供文件的路径,在这里文件名为myfile. txt
    • 不过这可以是文件系统中的任何位置,这里看到Paths. get方法,会把路径名字符串转换为Scanner能理解的一个Path对象
    • 最后要指定字符编码方式,一般都会使用UTF-8
Scanner in = new Scanner(Paths.get ("one.txt"),"UTF-8");
System.out.println("in = " + in);

输出:in = java.util.Scanner[delimiters=\p{javaWhitespace}+][position=0][match valid=false][need input=false][source closed=false][skipped=false][group separator=\,][decimal separator=\.][positive prefix=][negative prefix=\Q-\E][positive suffix=][negative suffix=][NaN string=\Q�\E][infinity string=\Q∞\E]

若找不到文件报错:Exception in thread "main" java.nio.file.NoSuchFileException: one.txt

注意:one.txt文件路径需要在项目文件下同级别

不能为创建的module文件下,或为它的同级别文件

  • PrintWriter()方法
PrintWriter out = new PrintWriter("one.txt","UTF-8");
System.out.println("out = " + out.toString());

此方法如果没有找到文件不会报错,会从新创建一个新文件

输出:out = java.io.PrintWriter@1540e19d

注意文件路径同上

输出 (println)

格式化输出,在控制台窗口上生成输出

double a =  1234.123456;
//输出小数点后三位,并且占6个代码单元,左对齐
System.out.println(String.format("%6.3f",a));
//输出结果为 " "
  • printf以需要的格式输出

    • 首先提供一个格式字符串,其中包含有关的格式信息,指出输出应该是什么样

    • 然后是你想要格式化的一个或多个值

    • System.out.printf( "Price:%8.2f",10000.0 / 3.0);
      
    • 这里的百分号%表示这是一个格式指令,对应传入printf的下一个值

    • 后面的这个f表示我们想要把它格式化为一个浮点数

    • 这里的8表示宽度为8

    • .2表示小数点后有两位小数

    • 10000.0 / 3.0是需要格式化的值,所以显示为Price: 3333.33

    • : 3333.33一共为8位,多的位数向前输出空格

    • d表示格式化整数

    • 使用s格式化字符串和其他对象

  • system.out.printf("%(,.2f",-10000.0 / 3.0);	// prints (3,333.33)
    
    • (左小括号 表示负数要用会计专用格式打印,也就是要包围在小括号里
    • ,这个逗号表示要生成十进制分隔符
  • String. format()有时你可能还不想生成这个输出,而希望先将它捕获到一个字符串里,然后存储这个字符串,以便以后修改,

    • 要提供一个格式字符串以及需要格式化的一个或多个参数

    • Stringmessage = String.format ( " Hello,%s. Next year,you'll be %d", name,age);
      
      • name是字符串,age是一个整数

6.0 控制结构

if语句

顺序结构;按顺序执行的代码程序(一条路走到底)

if(判断条件){                      
	语句体;                                     
}else if(判断条件){        	//else if 是可选的                                
	语句体;                                                                                                }                                                                                    
  1. if语句从if后面跟条件,这必须包在小括号里
  2. 这个条件可以是任何有布尔值的表达式也就是false或者true
  3. if语句后面的部分是这个表达式为true要执行的部分
  4. 如果不只一个语句需要将其包括在花括号中{ }
  5. 如果条件为false可以添加一个else部分这是 可选的
  6. 可以用else if放置多个条件
  7. 如果以上所有条件都不为true就执行这一部分
if (yourSales >= 2 * target){
	performance = "Excellent" ;bonus = 1000;
}										//后面的内容是可选的,可以省略这些部分,而只有一个if
else if (yourSales >= 1.5 * target){	
	performance = "Fine" ;bonus = 500;	//如果第一个if的条件不满足,但满足第二个if的条件就执行这部分
}
else if (yourSales >= target){
	performance = "Satisfactory" ;bonus = 100;
}
else{
	System.out. println( "You' re fired" ) ;
}

while循环语句

初始化表达式;                           
while(布尔表达式){                        
    循环体;                                                
    步进表达式;                                        
}                                                                        
  1. 首先是一个while然后把条件放在小括号里,条件检查放在最上端,先检查再执行
  2. 这个条件可以是任何有布尔值的表达式也就是false或者true
  3. 然后是想在循环中执行的一个或多个语句
  4. 如果不只一个语句需要将其包括在花括号中{ }
  5. 在这里进入循环,检查这个条件,如果条件为true就执行循环中的这个代码
  6. 再回到最上面再次检查条件,当这个条件为true时,就会一直循环,直到最终条件变为false此时退出循环执行后面的代码
int balance=0;										//已经攒的钱
int goal=100_0000;									//预期攒的金额
int years=0;										//年份的初始化	
int payment=1000;									//每次攒的金额
int interestRate=5;									//年利率

while (balance < goal){												//已攒的钱和预期目标
    balance += payment;												//每次攒多少钱					
    double interest = balance * interestRate / 100;					//利息interestRate
    balance += interest;			//计算复利
    years++;
}
System.out.println("You can retire in " + years + " years " );		//你可以在X年后退休  计算完成 X=80

do while语句

  1. 这个语句中条件检查放在最下面,先执行再检查,所以此循环首先会无条件执行一次
  2. 关键字do放在最上面,关键字while放在最下面
  3. 同样这样的条件要放在小括号里
  4. 对于do while语句,最上面没有任何条件,指示我们第一次进入循环,循环结束时才会检查这个条件
  5. 如果条件为true就回到最上面,如此继续
  6. do while 语句不太常用,不过有一种情况会经常用到,这就是读输入时
  7. 在这个例子中,我们希望打印输出之后询问用户,问他们是否还想继续,只要他们说还没准备退休,我们就要继续类加银行账户里的存款
  8. 不过为了合理起见,我们要直接执行一次,而不先问问题
  9. 之所以使用do while循环,是因为需要先执行一次循环体,然后才可以计算循环条件
public static void main(String[] args) throws IOException {
        Scanner in = new Scanner(System.in) ;
        System.out.print("How much money will you contribute every year? ");    //每年攒多少钱
        double payment = in.nextDouble();
        System.out.print( "Interest rate in %: ");                   //年利率
        double interestRate = in.nextDouble();
        double balance = 0;
        int year = 0;
        Stringinput;

        // upDateaccount balance while user isn't ready to retire
        //当用户不准备退休时更新帐户余额
        do{
           // add this year's payment and interest  加上今年的付款和利息
            balance += payment;
            double interest = balance * interestRate / 100;
            balance += interest;                        //计算复利
            year++;
            // print current balance            打印当前余额
            System.out.printf("After year %d,your balance is %,.2f%n",year,balance);
            // ask if ready to retire and get input
           // 询问是否准备好退休并获取信息
            System.out.print ( "Ready to retire? (Y/N)");//准备好退休了吗?	Yes停止,oN继续
            input = in. next();

        }while (input.equalsIgnulleCase("N"));	//equalsIgnulleCase不区分大小写
 }

for循环

for(初始化表达式1;布尔表达式2;步进表达式3){//初始化只运行一次,布尔表达式执行判定,步进每次循环时运行
循环体;    //具体的执行语句
}
  1. for循环首先有一个关键字for
  2. 然后有三个部分,这些部分包括在小括号里,用分号;分隔
  3. 第一部分要初始化一个变量,这里为i变量从1开始,这个初始化会在,第一次进入循环前完成
  4. 第二部分是一个条件,所以当i<=10时,我们就要继续循环,每次进入循环都要检查这个条件,检查条件之后执行循环体
  5. 这个特定的循环只有一个语句,所以这里不需要大括号, 说明: 原文是这样讲的,但我习惯于加一个花括号{ }
  6. 可以看到他会打印1,然后移到第三部分,for循环的更新部分,在这里表示这个i要加1,所有现在i变成2
  7. 然后回到循环最上面,检查i是否仍然小于等于10,确实如此,所以我们要打印2
  8. 接下来变成3如此继续,所以这是一个i1变到10的循环,包括10,每一步都让i1
  9. 在这个例子中i是一个新变量,从这里的int可以看到,这个变量只在这个循环语句中合法,出了循环它就不复存在了 i为局部变量
  10. 如果i之前就已经存在,而且你希望执行循环时使用同一个i,那就不要在这里指定类型,可以任意调整for循环的各个部分
  11. 例如可以让i0开始,或者运行循环直到i小于10,或者每次将i1,或者是将i2
  12. 实际上Java语言中并没有,要求你必须在初始化,测试和更新这三部分中都使用i
  13. 你可以初始化i测试j然后更新k,当然这是很糟糕的凤格,不要这么做
  14. for循环的本意是让一个变量在某个地方开始,到某个地方结束,并且在某个地方更新,这才是for循环正确的用法
for (int i = l; i <= 10; i++){
	System.out.println(i);
}

举例:

    Scanner in = new Scanner(System.in);
    System.out.print("How many numbers do you nee to draw ? ");      //你抽的彩票是有多少个数字的
    int k = in.nextInt();
    System.out.print("what is the highest number you can draw ? ");     //你能抽的最高数是多少次
    int n = in.nextInt();
    /** 彩票概率公式:
             *compute binomial coefficient n*(n-1)*(n-2)  ...*(n-k+1)/(1*2*3*...*k)         
             * 大意是: 一个彩票一个有K个数,你从中抽N个数,你抽中目标数字赢的概率是?
             */

    int lotteryOdds = 1;
    for (int i = 1; i <= k; i++)
        lotteryOdds = lotteryOdds * (n - i + 1) / i;        //彩票赔率

    System.out.println("彩票赔率 = "+"1:" + lotteryOdds);//若 K=6 N=6 比率为1:1  若K=4 N为=24 概率为1:134596

初始化的值也可以在外面,for循环的三部分是可选的

int i = 1;
for (; i <= 10; i++)
System.out.println("I = "+ + i);

for each循环

如果有这样一个简单的循环你要做的只是访问所有元素,这种情况下可以使用for循环的一个替代版本

称为for each循环,同样for each循环也有关键字for和小括号(),不过在小括号里有一个冒号:

冒号后面是你要迭代处理的集合,在这里就是我们的数组a,冒号:前面要声明一个变量,轮流将它赋值为这个集合中的每一个元素

int a = new a[100]
for (int element : a)
    System.out.println(element) ;

在这里,我们首先从第0个元素开始,然后是第1个元素,依次类推

注意:这里完全没有使用索引,element会在,第一次迭代时赋为a[0],然后是a[1],如此继续,

而且这里直接使用element,所以不需要使用a[i]构造,应当尽可能使用for each循环,

这意味着只要是想要轮流访问所有元素就应当使用for each循环,

switch语句

switch(表达式){
    case 常量值1:        //语句体的执行入口    不满足则进行下一个入口
    	语句体1;
   	 	break;//程序若满足case1即结束,将不再执行后续代码  break可以不加但若如此switch将穿透执行直到下一个break为止
    case 常量值2:
   		语句体2;
    	break;
    ......
    default:  //结束            若所有case都不能执行将执行default收尾
   		 语句体n+1
    break;
}            
  1. switch是多分支的一种更紧凑的形式,这种情况下你的决定只涉及一个值
  2. 在这里,在一个菜单系统里,让用户选择一个选项,1,2,34我们要读入用户的响应,现在根据这个值建立一个switch
  3. 如果是1就执行一个分支,如果是2就执行另一个分支,以此类推
  4. 如果choice1我们想做些处理;如果是2我们想做另外一些处理
  5. 当然你也可以直接使用if else if else if很多程序员确实会这么做,不过有些程序员更喜欢switch的这种紧凑性
  6. 可以看到这个语法有一组case,case后面是让你进入这个case的值,
  7. 然后这里还有一个defaut,如果以上所有case都不匹配就会执行这个分支
  8. 关于switch有一个古怪的地方,每个case之间需要用一个,有魔法的单词break分隔,
  9. 如果没有加break,会发生很糟糕的事情,会继续执行进入下一个case,这往往不是你想要的
  10. switchi语句只适用于最好根据整数,枚举值或字符串做决定的情况,
Scanner in = new Scanner (System.in);
System.out.print ("Select an option (1,2,3,4) ");
int choice = in.nextInt( ) ;
switch (choice){
    case 1:
    	...		break;
    case 2:	
    	...   	break;
    case 3:
   		... 	break;
    case 4:
   		... 	break;
    default:
    // bad input. . .
    break;
}
  • switchi语句中看到的这个break关键字也可以在循环中使用,在这种情况下它会终止循环

举例:

在这个循环中我,们需要做一些工作,最多做一百年,实际上就是类加利息,

但是如果在这100年结束之前,我们已经达到了自标于是我们想退出循环,就可以利用这个break做到,所以一旦执行这个break就会继续运行循环语句后面的代码,现在break已经不再是必要的

while (years = 100){
 balance += payment;
 double interest = balance * interestRate / 100;balance += interest;
 if (balance >= goal) break;
 	yearS++;
}

另一种逻辑,不使用break,用不同的方式完成测试检查,实现业务

在这里的做法是把条件反过来,如果我还没有达到目标,就要让years1还需要在这里测试条件

很多人不是很喜欢break,会使用boolean条件避兔break,break表示在这里跳出循环

while (years = 100 && balance < goal){
 balance += payment;
 double interest = balance * interestRate / 100;balance += interest;
 if (balance < goal)
 	years++;
}

  • Continue会跳过循环体的剩余部分
Scanner in = new Scanner(System.in) ;
while (sum < goal){
    system.out.print("Enter a number: ");
    n = in.nextInt();
    if (n< 0) continue;
    	sum += n; // not executed if n < 0
}

注意:

  1. 多个case后面的值不可以重复,switch的表达式只能是byteshortcharint ,string字符串、enum枚举
  2. case语句可以颠倒,break可以省略

三种循环的区别(复习):

  1. 若条件判断从来没有被满足过,那么forwhile将不执行,do while将执行一次
  2. for循环的的变量在小括号中定义,只有在循环内部才可以使用。(局部变量)
  3. whiledo-while循环初始化语句本身就在循环外,所以循环后还可以继续使用

—-使用循环的建议:凡是次数已定的场景多用for循环,否则用while循环

break】关键字的用法
1,可以用在switch语句中,一旦执行,整个switch语句立即结束
2,还可以用在循环语句中,一旦执行,整个循环语句立即结束打断循环。
continue】关键字用法 // 一旦执行,立即跳过当次循环马上开始下次循环

死循环:一直执行不能结束的循环
标准格式

while(true){
循环体;
}

7.0数组

//数组的声明
String[] names;  
Stringnames[];
int[] scores;
---给数组初始化分为,静态初始化和动态初始化
//静态初始化:初始化数组与给数组元素赋值同时进行
names = new String[]{"一","二","三","四"};  

//初始化数组与给数组元素赋值分开进行
scores = new int[4]; 
//给数组赋值
scores[0] = 01;  
scores[1] = 02;
scores[2] = 03;
scores[3] = 04;
  1. 数组是多个相同数据类型的组合,实现对这些数据的统一管理
  2. 数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型
  3. 数组属于引用类型,数组型数据类型是对象(object),数组中的每个元素相当于该对象的成员变量
  4. 数组一旦初始化以后,长度不可变
  5. 数组中每个元素的空间是一样的
  • 这里是一个整数数组
  • int+[]+你希望的数组的名字这里就是a
int[] a;	//这是一个可以保存整数的数组类型		数组变量声明
  • 要创建数组需要使用new操作符 new a[100] //这会构造一个包含100个整数的数组
int a = new a[100]	//创建一个包含100个整数元素的数组然后,把它保存在这个变量a中
  • 数组索引从0开始,这个数组的索引从099,数组的长度是a.length,这里length不是方法调用,单词length后面没有小括号()

  • 现在这个特定的数组长度为100,数组的长度不是合法索引,必须将数组长度减1来得到最大索引,在这里也就是99

  • 要使用中括号[]操作符来访问元素,所以a是数组,a[i]就是其中第i个元素

for (int i = 0; i < a.length; i++)
	System.out.println(a[i]);
}	//这里看到这个特定的循环允许`i`取值从`0`值到数组长度减`1`然后打印出第`i`个元素

如果你想跳过某些元素,或者如果你想以某种方式更新元素就要使用别的循环方式,

你也可以写一个循环或者通过一组单个的赋值语句填充一个数组,不过通常都希望能够在声明,一个数组的同时提供初始值

可以使用这种初始化语法,同样先声明类型int[],数组名然后是一个=这类似于,初始化一个整数的做法

不过由于现在有多个值,要把所有值放在一起,包围在大括号里{}值之间用逗号分隔,

所以在这种情况下Java编译器能,确定你声明了一个长度为6,它会把这6个数放在你的数组中

这个特定的数组有一个名字名为SMALLPrimes

int[]	SMALLPrimes = {2,3,5,7,11,13};
  • 有时你可能想创建一个数组,只是要把它传递到一个方法,你并不会把它存放到一个变量中,
  • 在这种情况下可以声明一个匿名数组,
new int[]{ 17,19,23,29,31,37 }
  • 数组变量并不包含数组,它只是包含数组的一个引用,它包含内存中存储这个数组的位置
  • 创建副本时,我们复制的只是那个引用,数组变量并不包含元素,它包含的是引用
  • luckyNumbersSMALLPrimes指向内存的同一个位置,所以他们的值是相同的
  • 如果修改luckyNumbers的值SMALLPrimes也会改变,因为他们实际上是同一个数组
int[] luckyNumbers = SMALLPrimes;
luckyNumbers[5] = 12; // now SMALLPrimes[5] is also 12
  • 如何复制一个真的数组,copyOf方法取一个数组,以及长度,就是你想复制多少个元素
  • 它会为你返回一个新数组,其中包含全新的值,所以现在它们是完全不同的数组

举例:在n个数里面随机抽K个数

public static void main(String[] args) throws IOException {

        Scanner in = new Scanner(System.in);

        //赌以下组合。它会让你变得富有!
        System.out.println("Bet the following combination. It'll make you rich!");
        //你能抽几个数
        int k = in.nextInt();

        //你能抽的最高数字是多少?
        System.out.print("what is the highest number you can draw? ");
        //你抽的组合一共有多少个数
        int n = in.nextInt();

        //用数字1 2 3 N 填充一个数组。
        //fill an array with numbers 1 2 3 . . . n
        int[] numbers = new int[n];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = i + 1;
        }

        //  画出K个数字,并将它们放入第二个数组中
        // draw k numbers and put them into a second Array
        int[] result = new int[k];
        for (int i = 0; i < result.length; i++) {

            //在0和n-1之间创建一个随机索引
            // make a Random index between 0 and n - 1
            int r = (int) (Math.Random() * n);

            //在随机位置拾取元素
            // pick the element at the Random location
            result[i] = numbers[r];

            //将最后一个元素移动到随机位置
            // move the last element into the Random location
            numbers[r] = numbers[n - 1];
            n--;

        }

        //打印排序后的数组
        // print the sorted array
        Arrays.sort(result);    //数组排序
        for (int r : result) {
            System.out.println(r);
        }
    }
}

二维数组

二维数组的初始化

//静态初始化
int[][] scores = new int[][]{{1,2,3},{3,4,5},{6,7}}

//动态初始化方式一
String[][] names = new String[6][5];
//动态初始化方式二
String[][] names = new String[6][];
names[0] = new String[5];
names[0] = new String[7];
names[0] = new String[8];
names[0] = new String[7];
names[0] = new String[9];
names[0] = new String[6];
  • 这是数字的一个二维排列,首先必须指定类型,维数组的类型是lnt[][]
  • 一个中括号对应行索引,另一个中括号对应列索引,这里数组名是magicSquare
  • 要有这些大括号来包围整个数组,另外每一行也分别用大插号包围
int[][] magicSquare ={
    {16,3,2,13},
    {5,10,11,8},
    {9,6,7,12},
    {4,15,14,1}
};
  • 而且你不想提供初始值,如果你有一个二维数组
int[][] magicSquare = new int[ROWS][COLUMNS];		//ROWS行数	COLUMNS列数
  • 要访问数组元素需要,提供两个中括号,第一个中插号提供行,第二个中括号提供这一行中这一列的元素

  • 实际上二维数组会存储为一维数组的数组

  • 遍历二维数组

for (int[] row : magicSquare)	//row是一个存储一个数组的元素
    for (int element : row)
    	do something with element
  • 二维数组是数据的一种表格排例

举例: 以一个二维数组,存储不同利息下每年的不同本息

public static void main(String[] args) throws IOException {
    final double STARTRATE = 10;
    final int NRATES = 6;
    final int NYEARS = 10;

    //将利率设定为10...15%
    // set interest rates to 10 . . . 15%
    double[] interestRate = new double[NRATES];
    for (int j = 0; j < interestRate.length; j++) {
        interestRate[j] = (STARTRATE + j) / 100;
    }
    //定义一个数组    用来存储年份和不同利率的金额
    double[][] balances = new double[NYEARS][NRATES];

    //将初始余额设置为10000
    // set initial balances to 10000
    for (int j = 0; j < balances[0].length; j++) {
        balances[0][j] = 10000;
    }
    //计算未来几年的利息
    // compute interest for future years
    for (int i = 1; i < balances.length; i++) {
        for (int j = 0; j < balances[i].length; j++) {

            //从上一行获取去年的余额
            // get last year's balances from previous row
            double oldBalance = balances[i - 1][j];

            //计算利息
            // compute interest
            double interest = oldBalance * interestRate[j];

            //计算今年的余额
            // compute this year's balances
            balances[i][j] = oldBalance + interest;
        }
    }
    //打印利率
    for (double N : interestRate) {
        System.out.printf("%8.2f"+"|",N);
    }
    for (double[] row : balances) {
        System.out.println();           //给行换行
        for (double element : row) {
            System.out.printf("%8.2f"+"|",element);     //格式化输出
        }
    }      
 }
//输出结果
    0.10|    0.11|    0.12|    0.13|    0.14|    0.15|
10000.00|10000.00|10000.00|10000.00|10000.00|10000.00|
11000.00|11100.00|11200.00|11300.00|11400.00|11500.00|
12100.00|12321.00|12544.00|12769.00|12996.00|13225.00|
13310.00|13676.31|14049.28|14428.97|14815.44|15208.75|
14641.00|15180.70|15735.19|16304.74|16889.60|17490.06|
16105.10|16850.58|17623.42|18424.35|19254.15|20113.57|
17715.61|18704.15|19738.23|20819.52|21949.73|23130.61|
19487.17|20761.60|22106.81|23526.05|25022.69|26600.20|
21435.89|23045.38|24759.63|26584.44|28525.86|30590.23|
23579.48|25580.37|27730.79|30040.42|32519.49|35178.76|

数组的默认初始化值

对于byteshortintlong而言:创建数组以后默认值为0

对于floatdouble而言,默认值是0.0

对于char而言默认值是空格

对于Boolean而言默认值为false

对于引用类型变量构成的数组而言默认值是null

8.0 需要任意精度时使用大数

  1. 如果intdouble的精度不够,请使用 整型:BigInteger或 浮点数:BigDecimal。将整数转换为大整数
  2. 有时候在你的计算中,那些基本类型不够用比如intdouble甚至是long因为那些类型提供的位数不够,进行操作会出现溢出
  3. 这些都是类而不是基本类型,所以需要创建这些类型的对象,再对这些对象应用方法
  4. BigInteger是在java.math包中
  5. 然而如果使用一个Biglnteger,就能让这些数任意大,Biglnteger可以存放任意精度的数
  6. 不能直接写a乘以a 不能用运算符连接对象,实际上必须写为a.multlply(a)
  7. 对于对象,必需使用方法调用,而不能使用运算符

BigInteger

  • java.add()
  • 减法.subtract()
  • 乘法.multiply()
  • 除法.divide()
  • 取余BigInteger result[] = bi2.divideAndRemainder(bi1) ;//求出余数的除法操作,数组第一个值为商,第二个值为余数
//声明一个`Biglnteger`类型的变量,现在需要使用`Biglnteger`类的`value0f`方法
//这里创建为100_0000的Biglnteger

BigInteger a = BigInteger.valueof(100);		//valueOf() 方法用于返回给定参数的原生 Number 对象值
a*a   //这是错误的 会得到一个语法错误
a.multlply(a)    
  

Java8为Biglnteger提供了常量ZEROONETEN

Java9又提供了一个常量Biglnteger. TWO

BigDecimal

9.0 JavaDoc

  • 类注释
    • @author 标识一个类的作者,一般用于类注
    • @Version 指定类的版本,一般用于类注释
    • @Since 说明从哪个版本起开始有了这个函数
    • @deprecated 指名一个过期的类或成员,表明该类或方法不建议使用
  • 方法注释
    • @param 说明一个方法的参数,一般用于方法注释
    • @return 说明返回值类型,一般用于方法注释,不能出现再构造方法中
    • @throws 可能抛出异常的说明,一般用于方法注释
    • @exception 可能抛出异常的说明,一般用于方法注释

3、对象和类

人们将对象的静态特征抽象为属性,用数据来描述,在Java语言中称之为变量;

人们将对象的动态特征抽象为行为,用一组代码来表示,完成对数据的操作,在Java语言中称之为方法,

1.0面向对象编程的基本概念

20世纪70年代:“结构化”编程。

  • 算法+数据结构=程序
  • 程序在共享数据上运行

20世纪80年代:面向对象编程。

  • 每个对象都有数据和方法。
  • 封装:只有方法才能访问对象数据。

所以如今我们喜欢把程序想成是,由相互协作的对象组成的,每个对象都有很明确的职责

在面向对象编程术语中,我们会说对象有数据,也就是它的内部表示

对象还有方法,可以利用这些方法处理对象,我们说对象是封装的,这表示,除了对象的方法,任何其他代码都无权访问对象数据

正如我前面说的,这样就为你提供了自由,可以根据需要重新组织代码,

现在Java是完全面向对象的,它就充分采用了面向对象范式,在java中除了上一课中见过的那8个基本类型,所有一切都是对象

术语解释:

类描述了它的实例,也就是对象如何组织,而且描述了方法的行为,所以类要描述数据结构和方法代码

可以把类想成是制作“小甜饼的模具,对象就是它制作的“小甜饼”
或者也可以简单地把类想成是,一个有相同行为的对象的集合,

查看一个特定的对象时,对象有行为.也就是利用它的方法可以做的事情

它还有状态,也就是对象中数据的当前设置,另外对象还有标识,所以可以有两个不同的对象,尽管它们有相同的行为和相同的状态

但仍创建为不同的对象,它们是不一样的,在面向对象编程中,我们会努力把程序组织为,一个协作对象的集合,所以需要找出定义这些

对象的数据和行为的类,根据经验,有一个名词/动词规则.按照这个规则,查看问题的描述时,可以找出其中的名词,通常这些就非常适合作为类

例如:

如果你在开发一个处理订单的系统,订单可能包括多个商品,你可能要完成订单,就可以考虑商品(Item),订单(Order)、购买(Purchase)

顾客(Customer)等这些名词,看看哪些适合作为类

动词通常是方法,例如可以向一个订单添加一个商品,也就是add,可以为一个订单发货也就是ship注意这些动词,它们总是与一个特定的对象相关,你要向一个订单添加一个商品,订单是很重要的部分,添加是应用于订单的一个方法作为参数还要有所添加的商品,为一个订单发货也是一样,订单是所处理的东西,对它做的处理就是发货

当然这是一个计算机程序,而不是真正模拟,所以为一个订单发货可能只是,在订单对象中设置一个标志

2.0使用预定义的类

Java类库中包含成千上万的类,其中一些类非常容易使用

使用新操作符构造实例:

调用new这说明要构造对象,接下来是类名Deta,然后是一对小括号(),不同的构造器会有不同的操作,有些构造器还需要补充信息

例如:new Scanner() 接下来你要提供System. in或文件路径作为参数

Date类的这个特定的构造器,不需要任何额外信息,它只是为当前时间点,创建一个Date对象,所以要这样构造这个对象

  • 变量存储了对象的一个引用,在这里变量birthday,存储了对象Date的位置
  • 左边是变量,变量中包含对象的一个引用,右边是对象,这是有某种结构的一个块
  • 一旦有了一个对象,就可以调用方法了
Datebirthday=new Date();	//Date对象描述一个特定的时间点,所有的对象都描述时间点,它们都是Date类的实例

现在可以调用方法:

  1. 首先是对象,然后有一个点.,这表示后面将是一个方法的调用,方法名 然后是小括号()其中可能包含参数
  2. 这个方法名为toString它不需要任何参数,这个方法会返回描述这个对象的字符串
Strings=birthday.toString();

创建一个变量的副本:

  1. 现在birthday和deadl ine是两个对象变量,它们都包含同一个对象的一个引用
  2. 这些变量包含的是引用,建立一个剔用的副本时,只是得到另一个引用,指向肉存中的同一个对象
deadline = birthday;

可以把对象变量设置为null:

  1. null是一个特殊的引用,它并不引用任何对象
  2. 如果要调用deadine. toString会有一个错误,不允许在null上调用任何方法
  3. 此时deadline根本不指向任何对象,所以没有一个可以调用这个方法的对象
deadline = null;

注意:不要对null调用方法。

  1. 为了避免这种情况,需要添加一个if语句,如果deadI ine不为null,那就执行方法中的代码
if (deadline l= null){
	deadline.toString();
}

Date是一个时间点,按UTC度量,这是格林尼治时间的官方缩写形式,这表示国际协调时间,由于历史原因缩写字母的顺序却颠倒为UTC

Java提供了一组丰富的类来处理日期,其中一个是LocaIDate,

LocalDate有更多专门处理日期的方法,因为它知道日、月和年等有关信息

LocaIDate没有时间分量,所以LocalDate实际上是要表示一个日历日期,这个类之所以名为LocalDate,是因为它没有所在地区的概念,这就是你通常在墙上的日历上看到的日期,LocalDate类使用了一种不同的方法来构造对象,它使用了工厂方法,这里有一个工厂方法now

这会返回一个LocaIDate,实际上在这个工厂方法中有一个隐含的构造器调用,不过有工厂方法确实非常方便,它们可以做构造器不能做的一些事情,现代API经常使用工厂方法

如果要创建一个对应某个特定目期的LocaIDate,要使用of工厂方法,并按这个顺序传入年、月和日

关于LocalDate对象,它们允许你做很多有趣的处理,LocalDate类在java.time包中定义

有些人通晓日期的所有处理,他们把这些打包在这个类中,我们只需要使用这个类,而不用担心它到底是怎么工作的

LocalDate rightNow = LocalDate.now();
LocalDate newYearsEve = LocalDate.of (1999,12,31);

我们可以把返回的对象,保存在另一个变量中,现在有了这个变量,你可能想处理这个对象的年、月和日,需要从对象中取出来

对象是不透明的,要从一个对象得到任何信息,唯一的办法就是应用一个方法

getYear方法从一个,LocaIDate对象得到年,getMonthValuegetDay0fMonth方法分别得到月和日

LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);
int year = aThousandDaysLater.getYear(); // 2002
int month = aThousandDaysLater.getMonthValue(); // 09
int day = aThousandDaysLater.getDayOfMonth();// 26

LocalDate是怎么做到所有这些的呢? 这些方法是如何工作的?

要有效地使用LocalDate类,实际上你并不知道这些,而且可能也不真正关心,这正是封装的强大之处


方法的术语:

访问器方法是不会修改对象的方法

更改器方法是修改或者可能会修改对象状态的方法

实际上LocaIDate类就没有更改器方法,LocalDate的所有方法都是访问器

例如:

LocalDate newYearsEve = LocalDate.of (1999,12,31);
LocalDate aThousandDaysLater = newYearsEve.plusDays(1000);

来看这里的plusDays方法,这里从1999年的新年前夜开始加上1000天,但这并不会改变newYearsEve,如果查看newYearsEve还是在1999年,实际上plusDays方法,会返回1000天后的一个新对象.所以这是一个访问器方法,而不是更改器方法

因为它只访问,newYearsEve对象的状态,如今尽可能减少使用更改器方法,被认为是一种好的做法,不过原先可不是这样

LocaDater类确实非常新这是Java 8中才增加的,之前 我们有另一个类名为GregorianCalendar

GregorianCalendar someDay = new GregorianCalendar(1999,11,31);
someDay.add(Calendar. DAY_OF_MONTH,1000);       //someDay has been mutated
int year = someDay.get(Calendar. YEAR);             // 2002

这里我要创建一个GregorianCalendar对象,同样还是1999年的新年前夜,可以看到它的字符串表示显示了这个对象的大量内部状态,我们并不需要知道这些信息,但字符串表示的设计者,认为这些都是有用的信息,现在我把它命名为someDay,因为我也可能查看1000天以后的日期

这个API完全不同这里在这个GregorianCalendar,对象上调用了add方法,要提供两个参数,一个是时间单位我想增加天数, 另一个是所增加的天数所以这里没有plusDays,而是有一个更通用的add,在这里我首先指出想增加什么,也可以增加周、月或者年数,当然也可能是天数,下面来执行这行代码,可以看到没有任何返回

实际上SomeDay已经改变,现在它已经与以前不一样了,要明确有什么不同,从它的年份就可以看出来,现在它的年是2002,月是8,日是26所以,尽管开始时是1999年的新年前夜,但现在已经被改为另外一个不同的日期,正是这个原因,我没有把它叫作newYearsEve而是命名为 someday
如果仔细查看 你会注意到 calendar de month8 而实际上1999年新年前夜之后1,000天 是9月由于很奇怪的历史原因老的 GregorianCalendar 类的乐从0开始记 0对应一月 local dead 类没有这么做
如今 不再有太多人使用 GregorianCalendar类了 这个类存在很多已知的问题
java.time 包中的多个日期类 正是设计用来取代 GregorianCalendar 类的 这里介绍这个内容 是为了让你看一个更改器方法的例子
如今有人认为 有些时候更改器方法还是必要的 但我们要尽可能的减少 使用更改器的方法 原因是 这样可以更容易的在并发程序中 共享对象
如果你的程序要执行多个线程,不可变的对象可以安全的在所有线程中共享 ,另一方面 如果一个对象可能被一个线程修改 ,而且其他线程 会在不同时间观察这个修改你就会得到不可再生的结果 所以要尽可能减少使用更改器方法


下面来展示一个具体使用 LocalDate 类的完整程序,运行这个程序时,他会显示当月日历

public static void main(String[] args) {       
        LocalDate date = LocalDate.now();
        int month = date.getMonthValue();//方法获取1到12之间的月份字段,返回月份,从1到12。
        int today = date.getDayOfMonth();//方法获取日期字段,返回月份中的第几天,从1到31
        //方法返回此日期的副本,并减去指定的天数
        date = date.minusDays(today-1);  // Set to start of month  设置为月初   (当月日期today  -1)

        DayOfWeek weekday = date.getDayOfWeek(); //方法获取星期几字段,即枚举DayOfWeek
        int value = weekday.getValue();// 1 = Monday,...7 = Sunday   1=周一... 7=周日
        System.out.println("Mon Tue wed Thu Fri Sat Sun");

        for (int i = 1; i < value; i++) {
            System.out.print("    ");
        } 
        while (date.getMonthValue() == month) {     //如果还是当前月份
            System.out.printf("%3d", date.getDayOfMonth());
            if (date.getDayOfMonth() == today) {
                System.out.print("*");
            } else {
                System.out.print(" ");
            }
            date = date.plusDays(1);     //方法返回此日期的副本,并加上指定的天数

            //如果是星期一则换行
            if (date.getDayOfWeek().getValue()== 1) {
                System.out.println();
            }
        }
        if (date.getDayOfWeek().getValue() != 1) {
            System.out.println();
        }
    }

在这里今天是星期四 七日 这个程序很聪明 知道这个月第一天是星期五 以此类推 ,下面来看这是怎么做到的 首先创建表示今天的 LocalDate 对象 由这个对象可以得到月和日

下面要从今天减去足够的天数 从而得到这个月的第一天 这就是这里所做的 所以 minusDays 就类似于 plus Days 只是方向不同一个是减 一个是加 下面使用另一个方法 getDayOfWeek 方法
利用这个方法可以得到星期几 也就是 week day 这会作为枚举类型 dayOfWeek 返回 需要对 weekday 做一些计算 从而能够在这里生成适当数量的空格 weekday 对象有一个名为 getValue 的方法,利用这个方法得到 value 1对应星期一 monday ``2是星期二 tuesday 如此继续 直到7是星期日 sunday 然后使用这个 value 在这里生成适当数量的空格

现在要做的非常简单 我已经打印了适当数量的空格 现在要打印这个月第一天 然后让这个对象向前推进到这个月的第二天 ,再到第三天 第四天如此继续 每次推进到星期一时 还要打印一个换行 使得每个星期一从新的一行开始 什么时候停止呢

达到这个月最后一天时就会停止 我怎么知道这是这个月最后一天呢 月份有一个烦人的性质 有些月份有31天 有些有30天 有些则有28或29天

不过这是一个非常简单的测试 如果有一个日期 将他向前推一天 如果月份有变化 (不等于当前月)这肯定就是这个月的最后一天了 LocalDate 类知道所有月份的长度 所以他当然知道什么时候推进 到下一个月 我们只需要在这里找出这一点 所以如果还在这个月里 就继续打印 继续向前推进 ,不过最终总会进入下一个月 这个时候我就会停止,最后这里还有些代码用星号标志今天,就是这里看到的代码 这里我们使用了 LocalDate 类的几个方法 最重要的是一个日期可以安全的自行推进到下一天 或者我可能不能这么说 因为毕竟他不会自己推进 不管怎么样他能安全的计算下一天 然后我再把这个引用存储到同一个变量中,所以通过这个程序 可以让你感受到 使用其他人写的类是多么强大的功能

3.0定义自己的类

首先来看一个 Employee 类 这个类可能有点人为刻意 不过对我们很有用 因为这样可以很容易的添加一些貌似 合理的方法 而且很容易提出一组不同的变化形式 来研究java中类的不同特性

我们的 Employee 类是这样的 Employee 对象存储了三种信息

员工姓名 ,工资以及员工的雇佣日期 注意指定这些数据时 会指定他们是私有的 private
因为所有局部状态都应当是私有的 然后指定数据的类型 在这里这个类型是 string 然后是这个数据的名字 在这里这个名字恰好就是 name , 然后还是一样 指定 private 类型,这是浮点数,然后是这个字段的名字,名为 salary

注意这里 在 Employee 对象内部 这个字段的类型本身是另外一个类 LocalDate所以 Employee 对象中包含一个LocalDate 的对象 这个对象名为 higherDay ,

Employee这个类定义了对象的数据结构 name,salary,hireDay

然后定义了构造器如何工作 public Employee

之后定义了所有其他方法的代码 这里你只看到一个方法getName 不过我们会为这个类写很多个方法 所以如果要写你自己的类 基本结构就是先为这个类 提供一个名字 再指定对象的数据结构 然后指定构造器和方法的代码

class Employee{
    // Fields	字段
    private Stringname;
    private double salary;
    private LocalDate hireDay;  
    
    // constructors	构造函数
    public Employee(Stringn,double s, int year,int month, int day){
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month,day);
    }
    // Methods 方法
    public StringgetName() {
        return name;
    }
}

构造器

构造器或称构造函数 用来初始化实例字段

实例字段通常是私有的,要由构造器中的代码来设置
这里是我们的 Employee类提供的构造器 他有五个构造参数
员工名起始工资 ,雇用日期的年、月、日 ,我们要设置 name ,设置 salary再设置 hireDay ;注意hireDay 设置为一个对象 就是用这个LocalDay 的工厂方法 构造的对象

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

下面来看这个构造器的一个具体调用 假设我们要构造这个Employee

new Employee("James Bond",100000,1950,1,1)

注意 这些构造参数 第一个参数是一个字符串 第二个是这个100000 这是一个整数
不过这会提升为一个浮点数 然后年,月和日分别为这三个值
这个构造器运行时会创建一个新对象 这个对象没有名字 没有存储在任何地方 只是已经创建 不过这是一个对象 他有三个实例变量 name salary hireDay

可以看到这些变量已经设置

name = "James Bond" ;
salary = 100000;
hireDay = LocalDate.of(1950,1,1);

注意在构造器的声明中 ,构造器名与类名相同 构造器都是如此
要使用一个构造器 总是必须使用关键字 new 后面是类名或者构造器名
反正是一样的 然后是构造参数 这就会构造一个对象

  • 构造器的任务是初始化字段
  • 方法的任务则是访问以及修改字段来实现这个方法的目的

这里有一个方法 他的目的是增加一个员工的工资 下面来看这个方法 名为 raiseSalary

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

方法都有返回类型 这个特定的方法不返回任何结果 他只是要增加工资 ;返回类型 void 就表示不返回任何值 double是参数类型 byPercent是参数名

所以要增加某个人的工资 我们要知道工资涨百分之几,这里完成计算 将 byPercent 除以100 再将 salary 乘以这个百分数 这样就得到了所涨的工资 raise 然后将 raice 加回到 salary字段

如果有很多Employee 当然了每个 Employee 都有自己的 salary 自己的 name 和自己的 hireDay
所以调用这个方法时 ,我们想知道增加哪一个Employee 的工资,这取决于放在点.前面的对象 假设把上面 new Employee("James Bond",100000,1950,1,1) 创建的这个 Employee 保存在一个名“number007的变量中 现在调用 number007.raiSalary(5) james bond` 就会得到5%的加薪

double raise = number007.salary * 5 /100;
number007.salary += raise;

下面来看这个计算 这里所做的就是将 number007salary 乘以5% 再将 number007salary 加上这个raise 如果是另外一个Employee 就会增加另外一个人的工资 看到这个调用时 你可能认为这个调用有两个输入 一个是员工 一个是加薪百分之几 我们把方法的一个输入叫做一个参数 所以你可能认为这有两个参数 .点左边的部分 以及小括号()里的部分 小括号里的部分 是方法定义中明确定义的 他有一个名字 有一个类型 这种参数称为显示参数

不过对于调用这个方法的对象 就是这里的 number007 这个方法代码中完全没有提到这个对象,类中有一个方法是隐含的,这个方法会在一个对象上操作,就是调用这个方法是点左边的那个对象;

所以这个对象称为方法调用的隐式参数,每个方法调用都有一个隐式参数 ,具体来讲就是调用这个方法的对象,另外可以有一个或多个显示参数,就是小括号里的部分

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

如果愿意 你也可以让这个隐式参数更明确一些 实际上他有一个名字,这个名字叫 this 完全可以忽略这个名字 this 不过如果你愿意,当然也可以加上这个名字 这里我明确的写为 this.salary 就是取当前Employee 对象的 salary 乘以这个百分数,然后将这个salary 记工资 增加 raise 即加薪

有些人喜欢在每个方法中都加这个点 这确实会让代码更易读一些,不过大多数人已经习惯了隐式参数的用法他们不会在每个方法调用中都加点


public static void main(String[] args) {

    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);

    // lraise everyone's salary by 5%
    for (Employee e : staff) {
        e.raiseSalary(5);
    }

    for (Employee e : staff) {
        System.out.println(e.toString());
    }
}

在这个示例程序中 我要具体使用这个Employee 类,这个例子确实相当简单 我们要建立一个包含 三个Employee 的数组 要在这个数组中填入三个Employee 对象 所以注意这里有三个构造器调用
每个构造器调用 会返回一个对象的引用 我们把这些对象引用 分别放在数组的一个槽中 在这个循环中 我会迭代处理数组中的所有Employee 将他们的 salary 分别上涨5%

关于对象他们有状态 ,每个对象都会记住自己的状态Employee 对象会记住 name salary hireDay
所以当我增加salary 时 每个Employee 对象 现在都会存储一个更高的工资

还有第二个循环 这里再一次循环处理这个数组
对于每个Employee 我要打印名字 name 上涨后的工资 salary 以及雇佣日期 hireDay

下面来运行这个程序 这里可以看到这些Employee 如果比较这些工资 你会发现 确实比构造这些 Employee 时的工资要高 这说明这个程序确实能正确工作

我要说明的另外一点是 这个程序有两个类 有一个类 EmployeeTest 其中包含一个main方法,这实际上就是我们所说的程序,工作就是要在这里完成,除了EmployeeTest 类 当然还有Employee 类本身就是这里,在这里我只是将 employeeTest 的类 和 Employee 类都放在同一个文件中 你可以这么做 或者也可以把它们分别 放在多个不同的文件中 这要看你的选择

    public StringgetName() {
        return name;
    }
    public double getSalary() {
        return salary;
    }
    public LocalDate getHireDay() {
        return hireDay;
    }
 //工资增加
    public void raiseSalary(double byPercent) {
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }

这里可以看到一些方法 其中三个方法非常简单 这里的这三个方法只是得到 name celery hierDay 字段,并返回 第四个方法是raiseSalary方法 你在前面已经见过

查看这些实例字段时可以注意到 在Employee 类的这个特定实现中 name永远不会改变,要用一个特定的名字构造Employee 另外 还有一个 getName 方法返回这个名字但是没有setName changeName 之类的方法 让我们修改这个Employee name

正如我在一开始说过的 这不是一个真实的Employee类 显然在真实生活中 员工完全可以而且也确实会改名
但在这个实现中 name是不能改变的 雇佣日期hierDay也不能改变 在真正的Employee 类中 这也是不能修改的 这是有道理的 员工只会雇佣一次

另一方面 工资 salary 则是可变的 就有这样一个方法 也就是 raceSalary 方法可以改变 salary 现在涨工资时 raceSalary 方法不会告诉你工资涨了多少 也不会告诉 你上涨之后的新工资是多少 要询问这个信息 必须调用 getSalary 方法 注意方法的返回类型 getSalary 返回一个 double

返回类型总是写在方法名前面 getHireDay 返回 LocalDate 同样写在前面

我在前面说过 raiseSalary 不返回任何结果 因此返回类型指定为 void

private私有修饰符

    private Stringname;
    public StringgetName() {
        return name;
    }

我们再来考虑一下 为什么要让一个类的实例字段是 私有的 也就是 private

name是一个私有字段 要让公众知道一个员工的名字 必须掉用这个方法 getName

所以这样做的第一个好处是 这样我们就可以实现只读字段 在构造器中设置一个字段后 他就不再改变
这意味着 绝对不会有人意外 修改了本部该修改的字段 这种情况不会发生 我们完全可以控制 如果有一个任何人都能写的变量 你知道的 定会有人用不应该的方式 修改这个变量 这只是时间早晚的问题

我们喜欢私有字段的第二个原因是 而且实际上这个原因也更重要 这样允许类的内部表示不断演化
例如假设我们的 Employee类的第二个版本 想要分别存储名和姓 可以想见 这样做可能有很多原因

如果 name 原本是一个 public 字段 那么原来肯定有很多代码访问这个字段
也就是读取 name 还会有代码写 name
现在如果把这个字段去掉 而代之以其他字段 就会出问题 这很糟糕

设计一个类时 实际上就是在与类的用户签订一个合约,用户应当能继续使用这个类 而不必一直更新他们的代码 这说明 在一个类中放入一个公共字段时 即public 字段,就是在承诺要一直保持这个类 这样一来 就很难用这里看到的做法改进这个类

	private StringfirstName;
    private StringlastName;
    public StringgetName() {
        return firstName + " " + lastName;
    }

另一方面 如果这些字段是私有的 除了你自己的方法 任何其他代码都不知道这些字段 而且你自己的方法也不会太多 这个特定的类只有很少的几个方法 这是很典型的 而且很容易更新方法

可以更新 getName 方法 使他现在根据名和姓得出名字 而不像第一个版本中那样直接返回name字段

现在这个类已经演化了 而这个类的客户并不需要知道这些 他们可以像从前一样 继续调用 getName 方法
他会继续正常工作 这正是封装的关键优点

正是这一点使我们能不断的演化类

4.0java中类的高级概念

现在你可以自己定义简单的类了实际上你完全可以定义相当复杂的类因为这确实也没什么

你要定义实例字段、定义方法、构造器。再把他们汇总在一起 与编程中很多其他方面一样 要想达到特殊的效果 成败在于细节
下面我们就要用很多很多课件 介绍这样一些特殊效果 尽管有这么多烦人的小细节 你也不要为此而灰心
他们大多数并不太常出现

final不可变的实例字段

首先有时你希望指示一个实例字段 不能修改 可以把它标志为 final 就像第三课中看到的局部变量那样
有些程序员坚定的认为 对于大多数实例变量都应当使用final 而另外一些程序员 却不认为这有那么重要 你只需要遵循你自己的编码原则

这里有一个 String声名为 final 所以这意味着 一旦设置了这个对象的 name 不管他是什么 这个 name 就永远都不能改变 不过只有当类是一个不可变的类时 才是如此

private final ptring name;

String有一个可变的版本 名为 StringBuilder 这里我有一个Employee 对象 他有一个字段 evaluations 这个字段的类型是 StringBuilder 我把它声明为 final 在构造器中 我把这个字段设置为 new StringBuilder 现在他的内容为空

这里有一个方法 他会在这个 evaluations 对象上 调用另一个方法 也就是 append 的方法 向这个评价追加某个字符串 显然 evaluations 已经改变 调用 giveGoldStar 之后的这个 evaluation 与之前的 evaluations 是不同的 这完全没问题

final 并不表示这个变量指示的对象 是不可变的 他只表示这个对象 绝对不会替换为另一个不同的对象
所以 没有人能够把一个不同的 StringBuilder 放在 evaluation字段中

这个字段总是开始时为空的这个对象 他一直是这里构造的这个对象 以上是final 字段

private final stringBuilder evaluations;
public Employee( ) { evaluations = new StringBuilder();.. .}
public void giveGoldStar() { evaluations.append( "Gold star!\n" ); }

Static静态修饰符

在《Java编程思想》P86页有这样一段话:

  “static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

  • static 变量是给类用的。这样类初始化的时候,就会给static进行初始化

  • 方便在没有创建对象的情况下来进行调用(方法/变量)

  • static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

有时还会看到字段的另一个修饰符 这里有一个类 其中定义了一个名为 nextId 的字段,这是一个整数 通过指定他为 static即静态字段

这说明 我并不希望每一个Employee 对象中 有一个不同的 nextId 字段

相反我希望是每个类一个字段 所以Employee 类中有一个 nextId 字段
可以这么讲 你可以认为它黏在Employee 类中 对于所有对象 如果引用 nextId 都只是这一个字段

 	private static int nextId; // one field per class	每个字段一个字段
    private int id; // one field per object				每个对象一个字段
    public void setId() {
        id = nextId;
        nextId++;
    }

为什么想要有这种静态字段呢 例如我可能希望有一个方法 setId 要把 id 设置为一个唯一的数 为此他将 id 设置为 nextId 的值 然后让全局的 nextId 1 这样以来 只要在一组Employee 上调用 setId
他们就会得到各不相同的 id 所以这里的 static 实际上表示 shared 也就是共享

为什么叫 static 而不是 share 的呢 这里有一个历史原因 在 c++里 这就叫做 static 而不是 share
那么为什么 c++中这么叫呢 他们当时不想再引入另一个关键字 由于与此无关的一个原因 已经有了 static 所以他们决定重用这个关键字 有一个鲜有人知的事实 c ++中可能有一半的关键字 都可以替换为 static 那样也是可以的 不过可能不是很清楚

所以如果看到 static 可以认为这是一种表示 share的 的古怪方式

public class Math {
        public static final double PI = 3.14159265358979323846;
        // Accessible anywhere as Math.PI
    	...
}

这里我们把这两个修饰符放在一起 就有了一个 static final字段 这表示这个字段要共享
而且没有人能改变他 所以这是一个共享常量 这实际上来自 math 类 他在标准库里可以看到
PI定义为 \(π\) 的值 这是一个可以全局访问的常量 他是公共的 任何人都可以通过 Math.PI引用这个常量

指定类名.字段名

按惯例,对于这些共享常量往往使用大写字母 我们已经见过了不属于任何对象的静态字段

static方法

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。

静态方法不在对象上操作 如:

Math.pow(a,b);

例如: Math类有一个静态方法 pow 以及很多其他这样的方法 pow方法有两个参数 a 和 b 然后他会计算 a 的 b 次方 他并不使用任何 Math对象 他只需要 ab 并使用他们完成一个计算

所以这是静态方法的一个很好的例子
这样一个方法中 只使用传入方法的参数 而不处理任何对象
还可以用另一种方式来考虑 静态方法没有 this 他没有任何隐式对象可以操作

在java中 现在每个方法都必须在某个类中 所以方法pow必须放在某个类中 但显然他根本没有引用任何对象 所以让他作为一个静态方法 是非常有道理的

让一个方法成为静态方法的一个原因 是你只想对传入的参数做某些处理 而且不需要任何对象

  • 静态方法只能访问静态字段

静态方法只能访问一个类的静态字段所以我们可以写一个方法 getNextId 它引用前一页例子中的 nextId 静态字段

public static int getNextId(){
    return nextId; // returns static field
}

不过 静态方法绝对不能引用类的实例变量 如在这里他就不能访问 name salary 因为静态方法不在对象上操作

int n = Employee.getNextId();

这里没有实例,当然也没有他能访问的实例变量 对于每个静态方法 调用时要指定类名 而不是对象名,点然后是静态方法名后面是可能有的任何参数 除了你之前见过的 powsqrt
math 方法是静态方法 你见过的另外一个方法 也总是静态方法 这就是 main方法 每个程序都有一个mian方法 这个方法是静态方法 因为一个程序开始运行时 还没有任何对象 还没有创建任何对象 实际上这正是main方法的任务 要开始创建对象并使用这些对象

下面的示例程序 会展示静态字段和方法的使用

private static int nextId = 1;
private Stringname;
private double salary;
private int id;

public Employee(Stringn, double s) {
    name = n;
    salary = s;
    id = 0;
}
//工资增加
public void raiseSalary(double byPercent) {
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
}

public void setId(int id) {
    this.id = nextId;
    nextId++;
}
 
public static int getNextId() {
    return nextId;
}

public StringgetName() {
    return name;
}

public double getSalary() {
    return salary;
}

public int getId() {
    return id;
}
@Override
public StringtoString() {
    return "Employee{" +
        "name='" + name + '\'' +
        ", salary=" + salary +
        ", id=" + id +
        '}';
}

我把Employee 类稍作简化 去掉了 hireDay 因为这个例子中不需要这个字段 相反我加入了一个 id字段
所以每一个 Employee 都有一个 id ,id 初始化为0

然后这里有一个名为 setId 的方法 可以调用 这个方法为 Employee 提供一个唯一的 id 这个唯一的 id 设置为 nextId字断 nextId字段是一个静态字段 ,一开始他的直为1所以第一个Employee 会得到 id1 第二个 Employee 会得到 id2 因为每一次调用 setId nextId 字段都会增1

setId() 不是一个静态方法 因为他要修改一个员工的 id 也就是一个Employee 实例的 id 所以他必须是一个实例方法

不过这里的getNextId 则不同 他只报告 nextId字段这是一个静态方法 他只访问一个静态字段

public static void main(String[] args) {
    // fill the staff array with three Employee objects

    Employee[] staff = new Employee[3];
    staff[0] = new Employee("Tom", 40000);
    staff[1] = new Employee("Dick", 60000);
    staff[2] = new Employee("Harry", 65000);
    // print out information about all Employee objects

    for (Employee e : staff) {
        e.setId();
        System.out.println(e.toString());
    }
    int n= Employee.getNextId();// calls stati method
    System.out.println( "Next available id=" +n);
}

在这个示例代码中 我们再一次用一组Employee 实例,填充一个Employee 数组 此时他们的 id 都为0
下面循环处理这些 Employee 对这些对象分别调用 setId()然后打印各个Employee 的信息 最后这里展示了 getNextId() 方法的使用

下面运行这个程序 这里可以看到输出:

//输出
Employee{name='Tom', salary=40000.0, id=1}
Employee{name='Dick', salary=60000.0, id=2}
Employee{name='Harry', salary=65000.0, id=3}
Next available id=4

可以看到这些 Employee id 分别是1 23 而且下一个可用的 id4
你可能会说 在这里分别为这些 id 赋值有点傻

当然 我们可以在构造其中做这个工作 所以可以在这里加入相应的代码 这也是一个不错的解决方案

public Employee(Stringn, double s){
	name = n;
    salary = s;
    id = nextId;
    nextId++;
}

顺便说一句 在 eclipse (IDEA)中 静态变量用斜体显示 所以可以看出 各个对象的变量(不是斜体)与静态变量的区别

static变量

static变量也称作静态变量,静态变量和非静态变量的区别是:

静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

static代码块

  static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

​ 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

下面看个例子:

class Person{
    private Date birthDate;
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

 isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

static关键字的误区

  • 与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。
  • 在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字
  • 静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)
  • 在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定

5.0java中的参数传递

java使用所谓的按值调用 这表示方法会得到所传入的值的副本

因此 如果你向一个方法传递一个变量 这个方法就会得到这个变量中内容的一个副本
方法并没有能力改变这个变量 下面来解释这是什么意思

public static void tripleValue(double x){ // doesn't work
	x = 3 * X;
}

来看这个tripleValue方法 他会报告将传入的参数乘以3 得到的结果 然后再把结果保存到这个参数
顺便说一句 这是一个静态方法 因为他只是处理传入的参数 这在java中是不行的 实际上这是行不通的

double percent = 10;
tripleValue(percent);

来看这里的调用 这里有一个变量 percent 我们把它设置为10
现在把它传递到 tripleValue我们想让 percent 变成他的3倍 但这并没有发生 这个调用之后 percent 还是原来的值

下面来看为什么这样不行 这里有一个变量 percent 这里它包含10 调用这个方法时 会创建一个新的变量 这就是参数变量名为 x 这是一个新变量,变量10会复制到这个变量,然后在这里 x 的内容会变为3倍,所以 x 确实是30

然后tripleValue方法退出,这个方法退出时,他的所有变量都会消失 包括参数变量 剩下的只有原来的 percent 变量 其中仍然包含10 就是这样

你要记住的是 由于java是按值调用的 所以你永远也不能 写一个直接修改变量的方法,在允许这样做的其他编程语言中 比如 c ++可以选择调用方法时 希望按直调用还是按引用调用 按引用调用时是可以更新变量的

public static void tripleSalary(Employee x){// works
	x.raiseSalary(200);
}

那么对象呢 这里我写了一个方法 名为 tripleSalary 这里接收一个Employee 我会把他的 salary 上涨20%

仔细想一下 这实际上就表示 salary 变为原来的3倍

下面来试试看 我要创建一个新Employee harry 把它传递到 tripleSalary 然后确实,harrysalary 变成了3

harry = new Employee(. . .);
tripleSalary (harry);

为什么这与前面对数字的处理有所不同呢?来看这里的示意图 这个图显示了 harry 这是一个变量

在Java中这是一个对象变量,实际上包含对象的一个引用 沿着这个箭头我会得到这个块,也就是对象现在调用这个方法,方法开始运行时会创建一个新变量,也就是参数变量,这个参数变量名为 x,可以看到他在这里
他会得到什么的副本他会得到引用的一个副本对不对

这里的变量包含一个引用 这个引用会复制到这个参数变量,现在 harry x 就有了同一个对象的引用 在这里确实很有用 现在可以写一个能修改变量的方法了 这个方法完成之后 局部变量消失 但是对象不会消失 对象还在这里 harry 仍指向这个对象 现在这个对象有了一个更高的工资 他很开心

这说明 java方法完全可以修改传入的对象 有些人说在java中 数字是按值传递的
而对象是按引用传递的 但事实上并非如此 在JAVA中 所有一切都是按值传递的,向个方法传递一个对象时
奥 等一下 对象不能传递 你能传递的只是一个对象引用 这个对象引用是按值传递的他的一个副本到达这个方法,如果可以按引用传递对象 你就能写一个交换两个对象的方法了,可以来试试看

public static void swap(Employee x,Employee y){// doesn't work
    Employee temp = x;
    x =y;
    y = temp;
}

这里的方法,试图使用通常交换两个东西的代码 来交换两个Employee 对象不过这是不奏效的来试试看

Employee alice = new Employee( "Alice",...);,
Employee bob = new Employee("Bob",...);
swap(alice,bob);

这里有两个Employee ,alice bob,现在我要交换 alice bob 但调用这个方法后 结果并不是 alice 指向名为 bob Employee 以及 bob 指向名为 alice Employee

我们没能交换 alice bob 的内容 为了说明这是为什么 下面再来看这个图

alice 是一个对象的引用 bob是一个对象的引用 现在我们把他们复制到 xy 中 也就是 swap() 方法的参数 现在这个 swap() 方法确实会交换 xy

swap() 方法完成之后 x 指向bob y 指向 alice 不过 swap() 方法退出后,他的局部变量就会被丢掉,alice bob并没有改变

Java从来不会使用按引用调用 他所做的就是在调用一个方法时,复制引用
这个示例程序包含了这个课件中看到的代码

public static void main(String[] args) {
    /*Test 1: Methods can't modify numeric parameters*/
    System.out.println("Testing triplevalue:");
    double percent = 10;
    System.out.println("Before: percent=" + percent);
    tripleValue(percent);
    System.out.println("After: percent=" + percent);

    /*Test 2: Methods can change the state of object parameters*/
    System.out.println("\nTesting tripleSalary: ");
    Employee harry = new Employee("Harry", 50000);
    System.out.println("Before: salary=" + harry.getSalary());
    triplesalary(harry);
    System.out.println("After: salary=" + harry.getSalary());

    /*Test 3:Methods can 't attach new objects to object parameters*/
    System.out.println("\nTesting swap: ");
    Employee a = new Employee("Alice", 70000);
    Employee b = new Employee("Bob", 60000);
    System.out.println("Before: a=" + a.getName());
    System.out.println("Before: b=" + b.getName());
    swap(a, b);
    System.out.println("After: a=" + a.getName());
    System.out.println("After: b=" + b.getName());
}

private static void swap(Employee x, Employee y) {
    Employee temp = x;
    x =y;
    y = temp;
}

private static void triplesalary(Employee x) {
    x.raiseSalary(200);
}

private static void tripleValue(double x) {// works
    x = 3 * x;
}

这里我们要测试的 trappaValue 方法,都可以看到这个程序运行时,实际上他并没有能力修改变量

下面我们来测试tripleSalary 方法 他确实能修改会传入的Employee对象

下面来测试 swap 方法,实际上 swap 方法不能改变这些Employee 指示的内容

你可以自己在运行环境中做这个测试 研究输出 然后再来看这些课件中给出的示意图 这确实是个好主意
对你很有好处;

//输出:
Testing triplevalue:
Before: percent=10.0
After: percent=10.0
 
Testing tripleSalary: 
Before: salary=50000.0
After: salary=150000.0
 
Testing swap: 
Before: a=Alice
Before: b=Bob
After: a=Alice
After: b=Bob

6.0更多的了解对象构造

你已经看到了如何写简单的构造器 现在来了解有关的一些技巧 以及如何处理更复杂的构造情况

首先一个类可以有不止一个构造器

StringBuilder messages = new StringBuilder();
StringBuilder todoList = new StringBuilder("To do: \n");

实际上 StringBuilder 类 这个类就是这样一个例子 可以创建构造一个空串的 StringBuilder
也可以为 StringBuilder 提供一个初始 String以此作为其时字符串

在这两种情况下 都必须使用构造器名 构造器名等同于类名 对于构造器的名字 我们别无选择
不过,这里实际上调用了两个不同的构造器
一个是无参数的构造器 一个参数为String类型的构造器

在这种情况下 我们说构造器名是重载

我们把方法名以及参数类型称为一个 方法的签名 或者如果这是一个构造器 则称其为构造器签名
所以我们有一个无参数的 StringBuilder()这是一个签名
StringBuilder 以及一个 String参数 则是另外一个签名

重载解析是指 编辑器会根据实参类型选择适当的版本 所以在这里,由于没有提供任何实参
所以编辑器会选择第一个构造器 在这里我提供了一个 String参数 所以编辑器会选择第二个构造器
如果我提供了一个其他类型的参数
比如说一个 double 编译器就会指出这是一个错误 我不知道该选哪一个
可以重载构造器 实际上可以重载任何方法

String.indexOf(int);
String.indexOf(int,int);
String.indexOf(String);
String.index0f(String,int);

例如 String类有四个不同的方法 都名为 indexOf 可能取一个整数或者两个整数 或者一个 String再或者一个 String和一个整数 取决于你如何调用 indexOf最后会调用一个正确的版本

  • 为重载解析正确的方法时 只查看参数类型
  • 返回类型不是方法签名的一部分
  • 返回类型也不会用于重载解析

假设有一个字段和一个类 如果未在任何构造器中明确的设置这个字段的值
在这种情况下 这个字段会在构造时设置为一个默认值

  • 所有数字将为0
  • 记布尔变量将为 false
  • 所有对象引用都为 NULL

这与局部变量不同 ,为初始化局部变量是一个错误,编译器会检测出这个错误 ,但如果有一个未初始化的字段 这并不是一个错误 只是会在构造时将它设置为默认值
对于null默认值必须特别当心,因为有时就会带来错误,这里我来给出一个例子

public Employee() { name = "";}
. . .
LocalDate h = harry. getHireDay();
int year = h.getYear();

这里有一个Employee 构造器 他将 name 设置为空串 但是出于某种原因没有设置 hierDay
可能是希望以后会有一个名为 setHierDay 的方法

现在调用 getHierDay我会得到一个 LocalDate 然后在返回的这个 LocalDate 上 调用一个方法
实际上 如果这个 LocalDate 从未设置他就是 null 那么当我在 null上调用一个方法时 NULLPointerException 就会中断程序的执行

所以一般来讲 写一个构造器时或者写多个构造器时 你要确保能初始化的所有变量确实得到了初始化

有可能一个类根本没有任何构造器 有时对于非常简单的类可能就是这样在这种情况下,会为他提供一个构造器 ,这个构造器只是将所有字段设置为他们的默认值

我喜欢这样记:如果一个类太穷了,支付不起自己的构造器,我们就会给他提供一个默认的构造器
不过这个构造器不是太好 他能做的只是一些默认操作

另一方面,如果一个类至少有一个构造器,就不会为他提供免费的构造器,不会再为他提供无参数的构造器

例如我们的Employee 类有一个构造器 这个构造器需要员工名字 起始工资和雇用日期,在这种情况下 就不能不提供任何参数的构造一个 Employee

如果你确实想这么做 完全可以自己提供一个无参数的构造器

public Employee(){};

因为构造器的体为空,它与默认无参数构造器所做的完全相同,也是将所有字段初始化为他们的默认值
在这种情况下 name hierDay null salary 0

实际上可以在构造器之外 为字段提供显式的初始值

class Employee{
    private Stringname="";
    ...
}

例如在这里我指出一个 Employee name 总是要构造为一个空串 当然以后可以再在构造器中修改
你可能想在某些构造器中 修改这个字段 使用这种初始化确实是个不错的主意

class Employee{
    private static int nextId;
    private int id = assignId();
        . . .
    private static int assignId(){
        int r = nextId;
        nextId++;
        return r;
    }
}

在这个例子中 初始值是一个常量 这是一个空串,不过这也可以是一个计算得到的值
例如:这里有一个 id 字段在,我用assignId()方法的返回值初始化这个id字段,

不论哪种方式可以看到 ,比如这里的name字段 ,还有这里的 id 字段,可以在构造器之外初始化一个字段

下面简单谈一谈构造器参数名

public Employee(Stringn, double s){
	name = n;
	salary = s;
}

Employee 类中,我使用了非常简单的名字,我使用 m 表示员工名字,用 s 表示起始工资,然后在这里我设置 name 等于 n ,salary 等于 s

单个字母的参数名可能不是很好 因为这会让代码很难读,所以更好的做法可能是

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

把他们命名为类似 aName aSalary 然后设置 name 等于 aName ,salary 等于 aSalary ;有些人非常不喜欢使用类似 aName aSalary 的名字

在这里你可以使用一个技巧 可以就把名字叫做 name 工资就叫做 salary

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

当然这样做的问题是 在这里提到 name 时,现在有两个 name 有一个参数变量 name 还有一个实例字段 name ,不过这里的技巧就是使用 this ,this.name 是实例字段

this关键字

三种用法
A,在本类的成员方法中,访问本类的成员变量.

B,在本类的成员方法中,访问本类的另一个成员方法.

C,在本类的构造方法中,访问本类的另一个构造方法 如://本类的无参构造,调用本类的有参构造

注意:this(…)调用,必须是构造方法的第一个语句。同super关键字thissuper不能同时使用,使用了this编译器就不再赠送super

super()和this()为什么不能同时出现在一个构造中?

java语言规定,在执行该类构造前必须先执行父类的构造,直到Object类的构造。

因此任何构造的第一句,必须执行父类的构造,如果没有添加super(),那么编译器会为该构造默认添加一个super()。如果使用super显示的调用父类构造,就用指定的那个父类构造,否则使用默认的无参构造。

但该类构造中出现了this()时,会在该构造中调用本类的其他构造,但最终还是会在调用链的底端调用到父类的构造。所以如果super()和this()同时存在,那么就会出现两次初始化父类。第一次是super()调用父类构造,第二次是this()调用链底端的子类构造里调用父类构造,这样就造成两次调用super。


没有 this.name 也就是JAVA语言中通常的写法则是参数 name ,所以 name 有两个含义
一个是这个作用域内的 name
java语言规则指出 如果没有this.限定 就是指参数name而不是实例字段 name 不过完全可以通过使用this. 点得到实例字段 ,this.技巧极其常用

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

这也是人们使用 this 的一个例子,这样一来 你就不必为这些参数变量另外起名字了

如果一个类有很多构造器,抽取出公共代码会很有意义,为此一种做法是让一个构造器调用另一个构造器 ,这是完全合法的

前提是 这个构造器调用必须作为第一个语句,调用另一个构造器时,这个调用必须使用关键字 this
而不是类名,尽管好像类名更有道理

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

所以在这里 我有一个只根据工资构造 Employee 的构造器,而不需要员工名,实际上员工名 name 是这样合成的,这个合成的员工名字以及工资,现在要传入另一个 Employee 构造器

不过奇怪的是 我们必须写 this 而不是 Employee因为语法是这样的 这里使用的 this 关键字,与 this 作为隐式参数没有任何关系

class Employee{
    private static int nextId;
    private int id;
    // object initialization block
    {
    	id = nextId;
        nextId++;
    }
    public Employee(. . .) { . . .}// constructor. . .
}

最后还可以采用另外一种方式 抽取公共的初始化代码,就是使用所谓的初始化块,这并不很常见,不过确实是合法的

在类的体中, 在这里我把它放在实例变量后面,不过你甚至也可以把它放在实例变量前面 ,你可以在类的体中放入任意的代码块 ,这些代码块用大括号界定,其中可以包含任意的语句

id = nextId;初始化 id 字段 ,然后nextId++更新静态变量 ,对于构造的每一个对象 都会执行这个代码

对于这样一个块,如果在前面加一个单词 static 作为前缀,其中的代码就只在加载类时运行一次,可以用来初始化静态变量

static{
	Random generator = new Random();
    nextId =generator.nextInt(10000);
}

所以在这里,把 nextid 静态变量设置为一个随机数 ,这里使用了一个随机数生成器, 静态初始化块可能很有用 ,如果需要做一些复杂的处理来初始化静态变量 ,可以使用静态初始化块 ,因为静态变量无法由构造器设置 ,使用对象初始化块更多的只是出于好奇

大多数人还是会把这个代码放在构造器中,在这个代码示例中,我汇总的前面看到的有关构造的所有内容,以及所有技术细节,把他们都综合到一个例子中 ,这样你就能自己运行和修改

private static int nextId;
private double salary; 
private int id;
private Stringname = "";
// instance field initializationprivate double salary;
// static initialization blockstatic
{
    Random generator = new Random();
    // set nextId to a Random number between 0 and 9999
    nextId = generator.nextInt(10000);
}
// object initialization block
{
    id = nextId;
    nextId++;
}
// three overloaded constructors
public Employee(Stringn, double s) {
    name = n;
    salary = s;
}
public Employee(double s) {
    // calls theEmployee (String,double) constructor
    this("Employee #" + nextId, s);
}
// the default constructor
public Employee() {
    // name initialized to "" --see above
    // salary not explicitly set--initialized to 0
    // id initialized in initialization block
}
public static int getNextId() {
    return nextId;
}
public double getSalary() {
    return salary;
}
public int getId() {
    return id;
}
public StringgetName() {
    return name;
}
@Override
public StringtoString() {
    return "Employee{" +
        "salary=" + salary +
        ", id=" + id +
        ", name='" + name + '\'' +
        '}';
}

这里有 Employee 类的另一个版本.他有一个静态字段,nextId 这里有一个实例变量 表示各个员工的 id
另外name初始化为一个空串 ,这里有一个 salary ,会在某个构造器中设置 ,但并不是所有构造器中都会设置 salary

public Employee(Stringn, double s) public Employee(double s)这两个构造器会设置 salary ,第三个构造器 public Employee() 什么都不做,他会把 salary 设置为0

这里有一个静态初始化块 他将 nextId 设置为一个随机数,程序运行时会执行一次,具体来说,就是在Employee 类第一次加载时运行

这里还有一个对象初始化块,每次构造一个对象时,都会执行这个初始化块中的代码

然后还有些平常的方法 getSalary()getId()getName()、、建议你自己运行这个程序
自己好好观察

public static void main(String[] args) {
    // fill the staff array with three Employee objects
    Employee[] staff = new Employee[3];
    staff[0] = new Employee("Harry", 40000);
    staff[1] = new Employee(60000);
    staff[2] = new Employee();
    // print out information about all Employee objects
    for (Employee e : staff) {
        System.out.println(e.toString());
    }
}
运行:
Employee{salary=40000.0, id=4158, name='Harry'}
Employee{salary=60000.0, id=5949, name='Employee #9876'}
Employee{salary=0.0, id=9875, name=''}

每一个对象是如何构造的,很有必要跟踪这个构造器调用 还有这个以及这个,他们分别调用了不同的构造器
然后问问自己,所有这些字段是如何设置的

8.0使用包和导包

介绍一个非常重要的内容,就是你一直都要使用的这就是包在java中,我们把相关的类组织在包中
例如 :

java.lang
java.util
java.time	...

在标准库中有一个名为 java.lang 的包 其中包含 java 语言中的核心类 如你已经见过的 Math 类或 System

还有一个 java.util 包 其中包含很多工具类 如 Date 类或 Scanner
另外 java.time 包收集了各种与时间相关的类 如 LocalDate

  • 使用包的一个原因是避免命名冲突

考虑 Date 这个名字 java.util 中有一个 Date
另外 java.sql 包中有一个 类也叫 Date 这个包用来处理与 SQL 数据库的交互 这是完全可以的
java.util.Date 类不同于 java.sql Date

所以通过使用包 我们可以避免命名冲突 java库使用包来组织类

你也可能希望对自己的类这么做 你想把它们放在你自己的包中,当然你也希望避免命名冲突
所以 你的包名要与所有其他人的包名不同这很重要
为此,标准做法是取你自己的一个名字、一个域名
例如 :
很多年前我就注册了域名 horsemann.come 我比所有其他名叫 horsemann 的人,都抢先了一步
你可能没有这么幸运,没能很早就注册你的名字,不过你肯定可以注册某个域名,或者如果你在一家公司任职
他们肯定已经注册了一个域名,取这个域名,把它倒过来写
这样 horseman.com 就变成了 com.horseman ,然后如果愿意还可以有子名,只是要确保他们总是唯一的
所以对于这本书中的示例代码可以有一个包 com.horseman.corejava

注意包不能嵌套java. util 包与java.util.jar包之间没有任何关系

对java来说 他们是有不同名字的不同包,如果有一个类 他在某个包里 总是可以用全名来访问这个类

或者按java的说法就是用其完全限定名来访问 所以例如对于类java. time . LocalDate
我们总是能用 java.time.LocalDate 来访问这个类

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

在这里我们说 java.time.LocalDate today 等于java.time.LocalDate.now()

但是当然这有些重复 为了避免这种重复 可以使用 import 语句

import 语句

import java.time. *:
LocalDate today = LocalDate.now();

使用 import 语句时 如 import.java.time.*

对于 java . time 包中的所有类 就可以不再重复加上java.time前缀了
所以在这里我可以只说LocalDate 而不用说java.time.LocalDate 还有这里只说 LocalDate 而不说 java.time.LocalDate 这正是 import 的目的 所以 import 并不是真正导入任何代码
不要把它与 c 或 c ++中的#include 语句混为一谈

  • import 只是导入名字 从而无需完全限定也能使用

也可以导入单个类的名字

import java.time.LocalDate;

在这里 我只想从java. time 导入 LocalDate 说明我可以使用 LocalDate
而不是java.time.LocalDate

但是对于java. time 包中的其他类,还是必须使用他们的完全限定名 导入时只能使用一层星号
不能有 import java.*.*这种用法

import java.util.*;
import java.sql.*;
...
Date today; 
// Error- -java.util.Date or java.sql.Date?

如果遇到这样一种情况 有两个带通配符的 import 就像这里一样
我们要导入 java.Utiljava.sql当然有可能这两个包都包含一个同名的类
实际上这里就存在这种情况 两个包装都存在 Date类

这样一来,如果使用 Date 而没有限定 编译器就会说哼 等一下我分不清你说的是这两个 dayt 中的哪一个
你是指java. util.Date还是java. sql.Date事实就是这样,对于这种情况你有两个选择

一:
使用 Date时明确的指定完全限定名,

二:

也可以再增加一个 import 在这两个 import 的后面再加一个 import java.util.Date
由于你特别的提到了java.util.Date这样一来,只要使用 Date这个特定的import,就能消除歧义

import 语句还有一个特殊的形式 用来导入静态字段和方法

import static java.lang.System.* ;

你要指出 import static 然后提供一个类名,后面可以提供一个通配符,这会把System 类的所有静态字段和方法 都导入到你的程序中

out.println( "Goodbye,world! "); // i.e.,System.out
exit(0); //i.e., System.exit

例如有这样一个 import 语句,静态导入java.long.System之后,我可以直接说 out.println而不用说 System.out println我可以说exit(0)而不必说 System.exit(0)

import static java.lang.System.out;

也可以不使用这里看到的通配符导入,还可以完成一个特定导入,可以只导System.out 或者更确切的导入 java.lang.System.out 这样同样可以直接说 out 而不是 System.out

这好像也没有减少多少工作,所以大多数人没有这么做,但这对于数学函数可能很方便 如果导入java.lang.Math中的所有静态函数 我就可以直接说 sqrt pow
而不用那么麻烦的说 Math.sqrt Math.pow 这就好多了一些

现代类库大量使用了静态导入,所以你可能会经常看到这个特性,

假设你准备把你自己的类组织到包中

package com.horstmann.corejava;
public class Employee{
   ...
}

在这里有一个 Employee类 它包含一些代码 这些代码对于这个讨论并不重要
不过我想把它放在 com.horsemann.corejava 包中

为此我要做两件事 首先需要加一个 package 语句,要把它放在这个文件的最上面 要放在所有语句前面,甚至是 import 语句前面,package语句 只是列出这个文件中的类 要放在哪个包里

顺便说一句 到目前为止 我们还没有使用过 package 一句,这意味着,目前我们写的所有类都在默认包中,这是一个没有名字的包

第二件事,是要注意把源文件放在哪里,源文件必须正确的放在目录树中,相应的子目录链要与包名完全匹配 在这里包名是com.horsemann.corejava这说明必须有一个目录 com,他有一个子目录 horsemann,horsemann 又有一个子目录 corejava
我要把 Employee.java文件 放在这个子目录中 ,所以要按这种方式组织文件,这非常重要 否则将无法正确的找到类

javac com/horstmann/corejava/Employee.java

从命令行编译时,会从基目录编译 这是包含 com 的目录 所以编译命令是javac com/horstmann/corejava/Employee.java毕竟 javac 要接受一个文件名
所以这里提供了带子目录的文件名 ,如果在 windows 中,就要使用反斜线 \而不是斜线/作为路径分隔符

下面来看实际的使用

package com.horstmann.corejava;
//the classes in this file are part of this package

import java.time.* ;
//  import statements  come after the package statement

public class Employee {
    private Stringname;
    private double salary;
    private LocalDate hireDay;

    public Employee(Stringname, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDay = LocalDate.of(year,month,day);
    }

    public StringgetName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    private void  raiseSalary(double bypercents) {
        double reise = salary*bypercents/100;
        salary += reise;
    }
}

这里还是考虑你已经见过很多次的 Employee类 我把它添加到这个包中,注意package 语句要放在所有 import 语句前面,就是要放在文件最上面

除此以外这个 Employee类没有什么特殊的地方,这还是我们在前面看到的同一个 Employee

另外还有第二个文件,他放在默认包中,这个包名为 packageTest这里导入了com.horstmann.corejava.*;在这种情况下 其实只导入了 Employee因为这个包只有这一个类 ,然后创建一个 Employee提高他的工资

public static void main(String[] args) {
    // because of the import statement, we don'thave to use

    //com.horstmann . corejava . Employee here
    Employee harry = new Employee("Harry Hacker",50000,1989,10,1);

    harry.getName();
    System.out.println(harry.toString());;

}

eclipse eclipse 会在这里显示报名和文件,他并没有具体显示任何层次组织 你在 eclipse 中看不到目录结构

要想看到目录结构 需要进入文件浏览器 ,这里是基目录, 可以看到它包含 `package Test源文件和默认包

这里是 com 子目录 ,打开这个子目录 其中包含一个 horsemann 子目录,然后有一个 corejava子目录 ,在这个目录中可以找到 Employee源文件

下面介绍如何在命令行编译这个程序 首先进入基目录,就是包含这个 com 目录的目录 ,然后运行 javac ,并提供包中文件的完整路径名 ,另一个文件没有包所以在这里直接编译 然后用通常的方式运行这个程序,不过现在我要让他稍微复杂一些

下面把 packageTest 也放在一个不同的包里 ,在 eclipse 中这很容易做到,只需要移动这个文件 ,可以看到会自动添加 package 语句

现在这个 import 就没必要了,当然现在编译这个文件时,必须指定完整的路径 另外运行时必须指定完整的包名,包名包含点而不是斜线 这就是让java运行的类 这里需要指出的是,javac 处理的是文件 所以这里看到的是斜线 而且可以看到这里的扩展名,而 java 处理的是类,所以java需要一个限定的类名,因此这里是

你已经了解了,如何把你自己的类添加到包中

我想再谈谈包的一些有点奇怪的特性 这些特性不是很常用 但你偶尔可能会用到 ,正常情况下 你会把特性比如实例变量或方法,声明为 public private 或者下一课还会看到,可以声明为 protected 不过也有可能完全省略 public private protected 修饰符

在这种情况下,这个特性只在这个包中可见 这称为包作用域,这说明这个包的所有类的所有方法,都可以访问他 这是一个有些危险的特性,这是默认作用域,这很糟糕

因为包作用域是完全开放的,任何类都可以把自己添加到任何包中 或者更准确的讲
稍后会看到可以添加到几乎任何包中

所以如果有一个诸如 com.horsemandan.cordjava 的包,你完全可以把类放在这个包中,而且添加的那些类可以访问我在自己的com.horsemandan.cordjava
加入的所有包作用域特性 ,出于某种原因 我可能忘记了将他们声明为 private
public 下面来看一个真实的例子

public class Window extends Container{
	StringwarningString;
    . . .
}

java.awt 包有一个类 window 这个类有一个实例字段,名为warningString
这个字段声明为一个 String,不过要注意 这里没有 private 这说明java.awt
中的任何其他类都可以访问 warningString实际上没有其他类这么做

所以这好像只是一个小小的疏忽,很多年前我第一次注意到这一点时,我说 奥
我可以把自己的类添加到java.awt包中我可以构造一个 window 可以把 warningString设置为 Trust me !

package java.awt;
...
window.warningString= "Trust me! ";

从这里的可以看到 这在java1.1中是完全可以的 所以可以注意到在某些情况下这不是好事情

当时sum公司有一个很简单的补救措施 他们修改了编译器 使得当时只有sum公司自己 能使用以javajavax 开头的包,所有其他人都不能向这些包添加类,这个特殊的保护措施到今天仍然有效,所以如今我不能再做这个攻击了 这解决了java创造者们的问题

但是我的问题呢 如果有人想进入我的包呢?这里有一种机制,您可以把包密封在一个jar文件中,但这是一个技术性很强的方法,并不是很常用

关于包,我们要讨论的最后一个问题是类路径,这也有些技术性,运行一个程序时有很多组件,这些组件会分布在不同的目录中,也可能分布在不同的JAR文件中

简单的讲 JAR文件就是一个包含多个类文件的文件,通常类库就会作为一个或多个JAR文件交付,启动java时需要告诉他,可以在哪些目录和哪些JAR文件中,找到他可能需要的所有不同的类

可以利用类路径来做到这一点,类路径就是一个列表,列出了所有这些目录和JAR文件
UnixLinuxMacOS 中类路径中的元素用冒号分隔
windows 中则用分号分隔,为什么 windows 中不用冒号呢,因为在 windows
冒号:用作驱动器符号,比如 c:

所以必须使用一个不同的分隔符 类路径中引用当前目录是很常见的 因为你经常,从包含大部分代码的目录开始运行,但是另外一些类却在其他目录中,当前目录使用符号.表示

shell中也使用点表示当前目录,实际上大程序可能会使用非常非常多的JAR文件
我曾经见过包含100个JAR文件的类路径,不用逐个列出所有这些JAR文件

有一种快捷方式可以提供路径名 然后是一个斜线 然后是*通配符 ,这会让java查
看那个目录中的所有JAR文件在 Unix 中要当心 ,你不希望*表示通配符展开
所以需要加引号 或者一个反斜线将它转义

在Unix中,必须以“*” 或 \*
java -classpath /home/user/classdir::/home/user/archives/archive.jar MyProg

可以像这样加上-classpass 选项 将类路径传递到java编辑器或java启动程序,这里可以看到 java -classpass

这里有一个类路径 包含三个元素一个目录、当前目录和某个jar文件,然后指定你想启动的程序

export CLASSPATH=/home/user/classdir:: /home/user/archives/archive.jar

以前人们常常使用环境变量来达到这个目的,他们会设置一个 classpast 环境变量把所有通常包含类的位置都放在这个环境变量里,如今这种做法已经不太常用了,因为人们越来越多的使用类库你的每个程序都会有一个不同的需求

所以更常用的做法是建立自己的一个启动脚本并以这种方式使用 -classpath如果没有正确的设置类路径 会发生什么 如果是这样 你的程序运行时 他将无法加载一个或多个类 你会得到一个错误消息
这些错误可能很难调试

所以 只要你的程序依赖于很多其他的库 不只是标准java提供的那些库 而是你的工作需要的第三方库 那就需要使用类路径 以上就是关于包需要了解的全部知识

下面我们将转向另一个重要问题 如何使用javadoc文档工具
你已经学习了如何使用包java 9 对于包的工作方式引入了一个重大变化现在可以将包分组为模块
可以这样做时的包是真正私有的或封装的而且”包私有”特性在包含这些包的模块之外是不可见的

对于大型程序设计这是一个极其重要的特性我们将在高级视频课程的第六课详细介绍这个内容


8.0使用javadoc工具生成类文档

写其他人想使用的类时当然需要提供文档,让使用者知道如何使用你创造的类
很早以前人们把文档写在一个单独的文件里 这很不好 ,代码和文档总有一天会相聚甚远以至于文档变得毫无用处只是时间早晚的问题

在java中则有一个好的多的方法,文档直接放在代码中,这样文档与代码就更有可能保持一致,因为更新代码的程序员可以同时更新文档

 /**
     *Raises this employee by a given percentage.
     *@param byPercent the percentage of the raise
     */
    private void raiseSalary(double byPercent) {
        double reise = salary * byPercent / 100;
        salary += reise;
    }

例如这里可以看到 Employee类 现在这个类中增加了文档注释 这里是 raiseSalary方法
可以看到一个文档注释,他以一个/**开头 可以看到这里有方法的一个描述 ,然后对于这个参数 byPercent 还有这个参数的一个描述 其中使用了一个标记 @param 是一个参数的文档 这里还可以看到@return 这是返回值的文档

还有一个特殊的工具名为 javadoc 稍后我会介绍如何使用这个工具 从源文件抽取 html 文档
实际上第三课介绍String类的 api 文档时你就已经见过这种 html 文档,那个文档就是从 String
类的具体源代码生成的 可以对所有内容提供文档

可以为包提供文档 可以为类和接口提供文档 这些文档出现在类和接口页面的最上面
当然还可以为各个公共和保护字段,构造器和方法提供文档
写注释时 要做到第一个句子可以独立成句 因为这句话要放在一个摘要表里 所以要好好整理你的想法 使得第一句话可以作为摘要

然后可以继续加上你想提供的 任何其他详细描述 在你的描述中,可以使用 html 标记来指定格式
<em> 表示强调<strong>表示粗题等等

如果你的注释包含代码 最好把他们放在特殊的标记中 就是{ }加 @code 这样你就不用操心要对小于号字符 (<)进行转义 如果你要使用 html 代码 那么其中的所有代码都会转换为 html

你就必须对他们进行转义 这是很麻烦的 还可以使用图像 只需把他们放在一个特殊的位置这里给出一个文档注释的例子

/**
 *Raises the salary of an employee.
 *@param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
 *@return the amount of the raise
 */
public double raiseSalary(double byPercent){
    double raise = salary * byPercent / 100;
    salary += raise;
    return raise;
}

这个注释在方法前面 可以看到一个 @parim 和一个 @return 这与前面例子中的那个方法是不同的
我只是想展示同一个方法的注释中 可以同时出现@paren@return

可以在类前面加一个注释 ,提供整个类的文档 ,类前面的注释可能很长 ,有些人喜欢在类前面
写一个完整的小手册 ,来说明如何使用这个类

/**
 * A{@code Card} object represents a playing card,such
 * as "Queen of Hearts". A card has a suit (Diamond,Heart,
 * Spade or Club) and a value (1 = Ace,2 . . . 10,11 = Jack,
 *  12 = Queen,13 = King)
 */
public class Card {
    /*The "Hearts" card suit*/
    public static final int HEARTS = 1;
    ...
}

这里可以看到描述一个公共字段的注释 ,还有其他的注释

可以使用 @author @version 来提供作者名和代码的版本
@since 标记很有用 你会在java文档中经常看到这个标记 他指出某个特性可能始于java1.1java1.8等版本

利用 @see 可以提供一些引用 这在 html 文当中会变为超链接 你可以引用其他包 可以引用任意的网页 或者如果使用 @see时不想要超链接可以直接在这里放一个任意的文本

对于包 文档以及介绍页面中显示的概述注释 这些注释要放在哪里 对此有一些特殊的规则 要抽取注是可以使用 javadoc 工具

javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...

首先是 javadoc 然后是你要处理的包的名字 可选的还可以用-d 选项 为文档提供一个目标目录

-link http://docs.oracle.com/javase/8/docs/api

增加一个-link 选项也很有用 这样一来 只要使用了标准类 如 String就会变成超链接
指向Oracle文档 还有很多其他选项用来指定文档的格式以及生成文档

这里提供了一个链接供你参考http://docs.oracle.com/javase/8/docs/technotes/guides/javadoc/

下面通过一个小例子来试试看 我们准备对 com.horsemann.corejava 包,运行 javadoc 工具
然后他会生成一组 html 文件

下面来看其中一个 html 文件,这里可以看到 Employee类的文档,可以看到他的方法,可以看到之前键入到源文件中的注释 下面快速的浏览一下源文件 能看到与这里相同的注释,这些注释抽取为这种有用的 html 格式 并且都提供了超链接 点击其中一个超链接 你会得到更详细的信息 包括返回值和参数的描述 这确实是一个绝妙的特性 要在你的代码中充分利用这个特性 必须承认这可能有点啰嗦 因为这里有一些重复,

 /**
     *Gets the name of this employee.
     *Greturn the employee name
     */
    public String getName(){
        return name;
    }

例如对于这个很是平淡无奇的 getName方法 他要得到这个员工的名字 要返回员工的名字 这看上去是重复的 但写这个注释不需要太长时间 而有了这个文档 使用这个类的人会非常感激你的

实际上我来展示一个例子 这是java.lang.String类的源代码,是 Oracle公司的人写的,可以看到很多javadoc 注释 这里是头部注释 这是构造器 他的注释 实际上比方法实现本身还要长 这里是另一个方法 同样有很长的注释 实际上不论简单或者复杂 java.lang.String 类中的每一个方法 都有一个javadoc 注释

既然 Oracle 公司的人能做到这一点你也能,


9.0有效的设计类

现在你已经了解了java中如何编写类的所有相关内容 这一课的最后我在针对如何有效的设计类介绍几个技巧

我们已经讨论过保证数据私有的重要性 要确保所有实例字段都声明为 private 另外要确保初始化所有实例字段 如果一个变量初始化为 NULL 导致程序以后因为 NullPointerException 而异常终止 这可不是好玩的

设计此类时 你肯定不想像设计数据库一样 在类中放入大量基本类型字段例如来看这里的 customer

public class Custome{
    private String street;
    private String city;
    private String state;
    private int zip;
    ...
}

可以看到他有一个 String另一个 String然后还有一个 String然后是一个整数 后面可能还有更多 String和整数

这表明 这里实际上有机会找出更多的类 街道城市周和邮政编码 这些共同构成了一个概念就是地址
因此应该把他们放在另一个类中

如果有一组数据在概念上是一体的 就应该把他们组合为一个新的类 这样一来现在 customer 有一个类型为 address 的实体变量

public class customer{
	private Address shippingAddress;
}

你的信息得到了更好的组织 所以如果你发现写了一个包含20多个实例变量的类 就要看看其中一些实例变量是否不属于这个类并把它们分组为多个类

另外对于一个实例变量我们往往喜欢为这个实例变量 自动的提供一个获取方法 和一个设置方法
实际上 eclipse 确实可以自动的为你生成这些字段获取方法和设置方法

但这可能不是一个好主意
例如

public class Address{
    private int zip;
        ...
    public void setZip(int newZip){ zip = newZip;}
        ...
}

在这个 Address类中有一个 zip 实例字段 我真的需要一个 setZip 的方法吗 ? 哼 有可能需要 不过也有可能不需要 可能我只需要一个名为 format 的方法 用某种合理的方式格式化这个地址

我需要一个 getZip 的方法吗 ?几乎可以肯定的说不需要 , 对于一个 Address对象 我想要多久修改一次他的 zip 编码呢
我可能更想直接创建一个有不同 zip编码的新对象

  • 所以不要自动的为每一个实例变量包含获取方法和设置方法

因为这样一来 就会违背,最初将他们声明为私有变量的初衷,对于有问题的类设计另一个标志是有一个庞大的类,包含非常非常多的方法 ,而这些方法并不都彼此相关

作为一个例子

public class CardDeck // bad design{
    private int[] value;
    private int[] suit;
    ...
    public void shuffle() { . . . }
    public int getTopvalue() { . .. }
    public int getTopsuit() { . .. }
    public void draw() { . ..}
}

来看一个 CardDeck类,其中包含很多方法
可以洗牌,可以得到最上面一张牌的面值210或者 a ,可以得到最上面一张牌的花色 ,黑桃或方块可以摸一张牌

如果再想想看 会发现其中一些方法处理的是一副牌 要洗一副牌 另外你想从一副牌里摸一张牌 有些方法则与一副牌无关 而只与一张牌有关 就是一副牌里最上面的一张牌
所以 最好把他们重新组织为两个不同的类

public class cardDeck{
    private card[] cards;
    ...
    public void shuffle() {...}
    public card getTop() {...}
    public void draw() {...}
}
public class card{
    private int value;
    private int suit;
    ...
    public int getvalue() {...}
    public int getsuit() {...}
}

cardDeck 有自己的一组职责 card的类则有另外一个职责 就是表示一张牌 当然如果类和方法名 能准确的反映他们所做的工作,这总是一个好主意

这好像不需要多说,但是如果查看java标准库 应该记得其中有一个 Date类 这个类并不表示一个日期
实际上他表示的是一个时间点 对于这个类 可能 pointingTime 作为类名更合适

类似的java库中还有一些方法 可以有更好的名字 所以对类和方法命名时确实要当心 这总是件好事
你要设计由其他程序员使用的类 多加一点小心总是好的

最后如今我们知道不可变的类确实是个好东西 如果你要为某个值建模 比如一个日期或一个时间点 你可能想处理这个值 ,但这种处理会生成新的对象

从这方面来讲 这个值有些类似于字符串或数字 这种情况下就很适合使用不可变的类 如果一个类是不可变的 你就可以在并发的线程间 共享他的对象 而不存在意外更改的风险 所以要尽可能的让类是不可变的

当然并不总是如此 比如说有一个表示员工的类 员工的工资会改变 或者对于一个银行账户类余额会改变 对于这些类 改变是他们很自然的一部分

不过再举一个例子来看日历日期 java1.1 GregorianCalendar 是可变的 java8中 LocalDate类可以提供完全相同的功能 但这个类是不可变的 所以不可变的类更胜一筹


10.0java的常用类

使用Timer(计时器)

使用Timer的schedule,schedule有3个参数:

schedule(TimerTask task, long delay, long period)

第一个为定时任务,根据业务需要重写TimerTask的run方法即可;

第二个为延时启动,单位毫秒;

第三个位多久运行一次,单位毫秒;

new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                   //做点什么(具体方法)
                   } catch (Exception e) {
                   e.printStackTrace();
                }
            }
        },0,5L * 60 * 1000);

使用String

contains();该方法是判断字符串中是否有子字符串

String类型有一个方法:contains(),该方法是判断字符串中是否有子字符串。如果有则返回true,如果没有则返回false。
//示例代码
Stringmap_String= "name,password,age,good,add"
if(map_string.contains("name")){
	System.out.println("找到了name的key");    //输出找到了name的key
}
if(map_string.contains("password")){
	System.out.println("找到了password的key"); //输出找到了password的key
}

使用StringBuffer

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()insert()reverse()setCharAt()setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

StringBuilder

StringBuilder类也代表可变字符串对象。实际上,StringBuilderStringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。

append();用于在列表末尾追加新的对象

append();//用于在列表末尾追加新的对象

//示例代码
l1 = ["Google", "Runoob", "Taobao"]
l1.append("Baidu")
更新后的列表 :  ['Google', 'Runoob', 'Taobao', 'Baidu']

insert();将指定对象插入到列表中的指定位置。

列表 insert();//将指定对象插入到列表中的指定位置。
L.insert(index,obj)   //ndex -- 对象obj需要插入的索引值(int类型)。obj -- 要插入列表中的对象。
    
//示例代码   
L1 = ['Google', 'Runoob', 'Taobao']
L1.insert(1, 'Baidu')
print ('列表插入元素后为 : ', L1)
列表插入元素后为 :  ['Google', 'Baidu', 'Runoob', 'Taobao']

reverse();对列表中的元素进行反向排序。

列表 reverse();方法对列表中的元素进行反向排序。

//示例代码
L1 = ['Google', 'Runoob', 'Taobao', 'Baidu']
L1.reverse()
print ("列表反转后: ", L1)
以上实例输出结果如下:
列表反转后:  ['Baidu', 'Taobao', 'Runoob', 'Google']

setCharAt();将字符串中指定的位置的字符替换成目标字符

setCharAt()
该方法是StringBuffer中的方法,主要作用是将字符串中指定的位置的字符替换成目标字符,setCharAt(int index,char ch)
index就是取代的位置 索引从0开始,ch是你要替换为的字符串。
//示例代码
StringBuffer str = new StringBuffer("good");
str.setCharAt(3,'s');
System.out.print(str);

输出结果:goos

setLength();方法的作用是设置字符序列的长度。序列将被更改为一个新的字符序列,新序列的长度由参数指定

setLength();
int i = 4 ;
public void setLength(int i);

//示例代码
public class setLength_1 {
    public static void main(String[] args) {
        // 创建一个StringBuilder字符序列可变的字符串
        StringBuilder st_b = new StringBuilder("Java World");
        
        //输出st_b的包含的字符串内容
        System.out.println("st_b =" + st_b);
 
        //输入字符串长度
        System.out.println("st_b.length() = " + st_b.length());
 
       //设定字符串的长度,产生新的字符串
        st_b.setLength(4);
 
        //输出st_b的当前包含的字符串内容
        System.out.println("st_b=" + st_b);
 
        //输出当前st_b的当前包含的字符串长度
        System.out.println("st_b.setLength() = " + st_b.length());
    }
}
//输出
st_b =Java World 
st_b.length() = 11
st_b=Java
st_b.setLength() = 4

4、继承

1.0理解和定义子类

继承是由已有的类构建新类的过程,新类从一个已有的类继承特性

例如: 依旧建立Employee类,现在考虑一类特殊的员工比如经理,在很多方面经理就类似于所有其他员工 ,但在一些方面他们是不同的,有些公司里如果经理能达到预期业绩,就能拿到奖金,这与普通员工不同,所以每个经理同时也是一个员工,但是并不是么每个员工都是经理,如果查看所有经理的集合,这会是所有员工的子集

我们说表示经理的类,是表示员工的类的一个子类,反过来我们说Employee类是一个超类,所以只要一个类继承了另一个类,这个类就成为子类,而他继承的那个类则是超类

下面来看java中如何定义子类

public class Manager extends Employee{
	added methods and fields
}

这里给出了 Manager 类定义的框架可以看到 Manager 定义为一个类这与其他类是一样的
不过现在他后面还跟了一个关键字 extends 这表示我们在继承,然后是超类 Employee
所以 Manager 扩展了 Employee

因为他增加了功能在 Manager类中会有增加的方法和增加的字段,下面加入这些方法和字段
这里有一个字段

public class Manager extends Employee{
    private double bonus;
        ...
    public void setBonus (double bonus){            
    	this.bonus = bonus; 
    }
}

用来保存经理能拿到的奖金,这里还有一个设置这个字段的方法,当然这些方法只存在于 Manager 类中Employee 类中是没有的

不过 Manager 还能从 Employee继承他的全部功能,例如应该记得 Employee 有一些方法 getName
getHierDay getSalary raiseSalary所有这些方法都会自动继承到 Manager 类中没有必要再次写出来

另外 Employee 类的字段也就是超类的字段会出现在所有 Manager 对象中,因此查看一个 Manager 对象时他也有一个 name一个 salary 和一个 hierDay

2.0子类的覆盖方法和构造器

覆盖(Override)

有时候可能一个子类继承了一个方法但是这个方法并不合适在这种情况下,就需要在子类中覆盖这个方法
在我们的例子中getSalary 方法对于经理就不合适,因为 Employee 提供的 getSalary 方法,只返回工资而没有奖金
所以我们想修改这个方法让他返回工资和奖金之和

来做第一次尝试

public class Manager extends Employee{
    ...
    public double getSalary(){
    	return salary + bonus;// won't work
    }
}		//错误尝试

getSalary 方法返回 Salarybonus 之和,听起来很简单不过遗憾的是这是不行的
salary Employee 类的一个字段,而不是 Manager 类的字段,他会直接出现在 Manager 对象中,但是 salary 仍是 Employee 的一个私有字段,没有一个子类方法能访问超类的私有字段

怎么办呢?哼 还是采用通常我们想要从对象,得到信息时的做法我们会使用一个方法,而不是访问一个私有字段,访问 salary 字段的方法名为 getSalary所以只需要问 Employee 你的工资是多少
然后加上奖金这就是 Manager的工资

遗憾的是这还是不行

public double getSalary(){
	return getSalary( ) + bonus; // still won't work
}

如果仔细看这里的代码会问这个 getSalary到底调用的是哪一个方法,他调用的正是我们现在正在定义的这个方法,这是一个递归调用所以我们要想办法避免这种递归规

我们需要很具体的指定,想要调用哪一个 getSalsry方法,我们不想调用 Manager getSalary方法

super关键字的三种用法
  1. 在子类的成员方法中,访问父类的成员变量
  2. 在子类的成员方法中,访问父类的成员方法
  3. 在子类的构造方法中,访问父类的构造方法

我们想调用的是 Employee getSalary 方法,处理这个问题有一个特殊语法,就是关键字 super

public double getSalary(){
	return super.getSalary( ) + bonus;
}

如果调用 super 加某个方法, 编译器会生成一个调用,不过不是调用当前方法,而是调用超类中定义的同名方法,这正是我们需要的,所以一个 Manager 计算工资时,首先想知道,超类是怎么计算的,然后使用超类计算的结果,再加上奖金这就是子类要做的计算

接下来我们来看如何构造子类对象

public Manager(String name,double salary, int year,int month,int day){
	super(name,salary, year, month,day);
    bonus = 0;
}

这是 Manager 类的一个构造器,要构造一个 Manager对象要由于 Employee 构造器相同的构造参数:名字、起始工资、雇用日期的年、月、日

当然我们想做的是设置 Employee name,Employee salary Employee hierDay同样的这些是 Employee 超类的私有特性

所以除了通过公共方法,没有其他办法访问这些字段,在这里我们想调用超类的构造器,为此也要使用关键字 super不过现在后面不再加.和一个方法名,他后面只跟一对小括号,以及小括号里的构造参数列表

调用超类构造器时,他会初始化超类的所有字段,然后,我们还要在这里初始化子类的字段写这个构造器时,一定要确保超类构造器调用的,是这个子类构造器中的第一条语句

如果省略调用超类构造器,会发生什么? 在这里会用超类的无参数构造器,构造超类对象,当然为此超类必须有一个无参数的构造器,在 Employee类的例子中并不是这样,在这种情况下如果子类构造器中没有调用 super,编译器就会报告一个错误

在上一课中我们把一些 Employee 对象,放在一个名为 staff Employee 数组中然后以某种方式处理这些对象,下面来做同样的事情

staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tony Tester", 40000,1990,3,15);

只不过要让其中一个是 Manager,而不是 Employee,这是完全可以的,Manager 也是一个 Employee
所以完全可以把一个 Manager 引用放在一个类型为Employee 的变量中这是合法的

现在来对这个数组中的所有 Employee 对象做些处理,

for (Employee e : staff){
	System.out.println(e.getName() +" " +e.getSalary());
}

对于每一个对象,我们要打印名字和工资要调用哪一个 getName 方法,嗯这很清楚只有一个 getName 方法,也就是 Employee 中定义的 getName 方法, 要调用哪一个 getSalary 方法, 这可有点麻烦现在有两个 getSalary 方法,Employee 定义了一个 getSalary,Manager 也定义了自己的 getSalary
所以这不是编译器能做的决定,这会发生在运行时

到底调用哪一个方法,取决于对象 e 的实际类型,如果e是一个 Employee 引用,就会调用 Employee getSalary 方法,如果 e 是一个 Manager 引用,则会调用 Manager getSalary 方法

多态

在面向对象编程中,我们有一个术语叫多态,这表示一个变量可以指示多种类型,多态是希腊语,表示多种形态,在我们的例子中,e可以是多种形态之一,或者更确切的讲是多个类之一,它可以是一个Employeee,也可以是一个 Manager

在诸如 e.getSalary 的调用中,具体选择哪个方法,取决于 e 的实际类型,这种现象称为动态绑定,这发生在运行时,虚拟机提供了某种机制,来询问一个对象的实际类型,然后会选择正确的方法这是一个重要的机制

因为这就使得程序是可扩展的,我们可以以后再增加其他类型,例如可以考虑其他 Employee类型 , 如钟点工或主管这些新类型中,对于这些方法可以有自己的方法实现不过我们不用修改程序的逻辑

例如,这里看到的循环完全可以正确工作 , 而不论我们增加其他多少 Employee类型,对于每一个类型都会调用正确的方法来得到名字和工资
这就使我们的程序是可扩展的,以后我们可以增加更多的类型,而不用改变程序的核心结构,下面把所有这些内容,汇总到一个示例程序中

public class Manager extends Employee {

    private double bonus = 0;
    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        bonus = 0;
    }
    @Override
    public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }
    public void setBonus(double bonus) {
        this.bonus = bonus;
    }
}

这是我们见过很多次的 Employee 类,这是 Manager 子类,可以看到这里只有一个实例变量 bonus
然后是构造器 getSalary 方法,这会覆盖超类的 getSalary 方法,然后是 setBonus 方法,超类中根本没有这个方法,这是一个新方法,就这么多所有其他特性都从 Employee 继承

这是我们的测试程序

    // construct a Manager object   构建管理对象
    Manager boss = new Manager("carl cracker", 8_0000, 1987, 12, 15);
    boss.setBonus(5000);        //奖金5000
    Employee[] staff = new Employee[3];
    // 用Manager和Employee对象填充staff数组
    // fill the staff array with Manager and Employee object
    staff[0] = boss;
    staff[1] = new Employee("Harry Hacker", 5_0000, 1989, 10, 1);
    staff[2] = new Employee("Tommy Tester", 4_0000, 1990, 3, 15);

    //打印所有员工对象的相关信息
    //print out information about all Employee objects
    for (Employee e : staff) {
        System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
    }
输出:
name=carl cracker,salary=85000.0
name=Harry Hacker,salary=50000.0
name=Tommy Tester,salary=40000.0

在这里我们要建立一个包含三个 Employee 的数组,注意这个数组的类型是一个 Employee 数组
在这里我们要分配三个对象,staff [0]是一个 Manager对象,前面我说过这是完全可以的
Manager 也是一个 Employee,这里我们还要分配另外两个 Employee然后逐个处理所有这些对象
打印他们的信息

查看第一个对象时,可以注意到他的工资是8万,奖金是5,000,我们调用 getSalary 时,这两个数正确相加,这说明调用了正确的 getSalary 方法,

关于方法调用再多做一点说明

x.f(args);

假设 x 是一个类型为 C 的值,C 表示某个类 C,另外有一个方法调用 x.f,这里 f 是方法
然后传入一些参数,看到这个编译器会做什么
首先编译器需要找到 f 的所有可能的选择,在java中方法是可以重载的所以可能有多个版本,所以他会查看类本身,以及他的所有超类中有给定名的所有方法

这里说是所有可访问的方法,这表示这些方法必须是公共的,或者可能是包可见的,一般来说应该认为是公共方法,可能有些方法的参数类型不匹配,这些方法将被舍弃,换句话说编译器会缩小范围,只考虑那些重载解析成功的方法,应该只有一个这样的方法
如果有多个编译器就会报告一个错误,并指出我无法决定要调用哪一个方法

这里有一些特殊情况,如果方法是 private static final编译器清楚的知道要调用哪个方法
稍后我们会介绍 final 方法

在这种情况下编译器会直接调用那个方法,不过这种情况并不常见,大多数情况下,编译器都需要生成代码,从而能够在运行时调用正确的方法,这就是动态绑定,

关于覆盖方法有两个问题需要注意:

首先覆盖一个方法时在你提供的新方法中,要求参数类型完全匹配,例如

class Employee{
	public void setBoss(Employee boss){ ... }
}
class Manager{
	public void setBoss(Manager boss) { ...}// Error-unrelated method
}

假设 Employee 类有一个方法 setBoss,在这里我们可以把一个 Employee 的老板
设置为另一个 Employee引用,在 Manager 类中,我们覆盖了 satBoss,在这里实现者可能认为
经理的老板肯定不会是一个底层员工,他肯定也是一个经理,所以实现者把参数类型,从 Employee 改成了 Manager,不过这样是不对的,现在 Manager 类实际上有两个 setBoss 方法,一个有 Manager参数,还有一个是从 Employee 继承的这可能不是我们想要的

所以在这种情况下一定要小心,要保证参数类型相同,这确实是一个很常见的错误,有一个防范这个错误的方法,覆盖一个方法时,用一个 @Override 注释标记这个方法
就像这样

@Override public void setBoss(Employee boss)

如果没问题,编译器会悄悄的接受,另一方面如果你犯了我刚才说的错误,就是没有把参数类型写为 Employee,而是写为 Manager,编辑器就会说这个方法没有覆盖任何方法,它会生成一个错误,然后你就能修正你的错误

所以可以看到,对于参数类型在一个覆盖方法中你别无选择,要与所覆盖的方法完全一致,不过对于返回类型情况则不同了,这里 Employee类中有一个 getBoss 方法,这个方法没有参数,现在我要用另一个 getBoss 方法覆盖他

class Employee{
	public Employee getBoss() { ... }
}
class Manager{
	public Manager getBoss() { ... } // 0k
}

这个方法也没有参数,所以他确实会覆盖原来的那个方法,但是他的返回类型有变化,这是可以的
只要返回类型是所覆盖的那个方法返回类型的一个子类型,这就是允许的所以在这里,Manager 的老板总是另一个 Manager,这是没问题的这是一个很有用的特性,有时称为有协变的返回类型

重写和重载的区别
  • 重载(Overload):方法的名称一样,参数列表不一样,同样一个方法可以根据输入参数列表的不同,做出不同的处理。普通方法和构造器方法都能够重载。
    • 方法名必须相同,参数类型,参数个数,参数顺序不同
    • 方法返回值可以不同
    • 访问修饰符可以不同
  • 重写(Override):在继承关系中,方法的名称一样,参数列表[也一样] ,当输入参数列表一样时,要做出不同于父类的逻辑功能,就是覆盖重写父类方法

重写特点:

  1. 创建的是子类方法,则优先用子类方法 (先用本身,没有再向上寻找父类)
  2. 重写时可以修改访问权限修饰符和返回值,方法名和参数类型及个数都不可以修改;
  3. 抛出的异常范围小于等于父类, 若大于则报错
  4. 访问修饰符范围大于等于父类
  5. 父类方法访问修饰符为private/final/static,则子类不能重写该方法,但是被static修饰的方法能够被再次声明。
  6. @Override:写在方法前面,用来检测是不是有效的正确覆盖重写. @:注解
    • 注意:这个注解就算不写,也是正确的覆盖重写. @Override是一种可选的注解工具,但非常建议使用
继承的说明:
  • 子类方法的返回值必须【小于等于】父类方法的返回值范围 (是指方法的定义的数据类型如 String、Object)
    • 因为子类中的方法名和参数列表都必须跟父类中的相同,所以返回值只有两种情况:A和父类相同、B父类中返回类型的子类
    • 类型转换,子类不能大于父类,小精度无法转换为大精度
    • 覆写就是可以沿用父类的方法,但是沿用父类的该方法的返回值类型【这其实又涉及到了多态】
  • 子类方法的权限必须【大于等于】父类方法的权限修饰符
    • 提示: public>protected>(default)>private // (default)指的不是该关键词而是留空不写的默认值)

继承关系中,父子类方法的访问特点:

A,子类构造方法当中有一个默认隐含的 super() 调用,所以一定是先调用的父类构造,后执行的子类构造 (两者都执行)
B,子类构造可以通过super关键字来调用父类重载 //是否有参数,输入对应数据类型来重载同名不同功能的方法
C,super的父类构造调用,【必须】是子类构造方法的第一个语句.不能一个子类构造调用多次super构造

总结:子类必须调用父类构造方法,不写则赠送super(); 写了则用写的指定的super调用,super只能有一个,还必须是第一个.

3.0理解java中的高级继承

有时候你可能希望类不能被扩展在这种情况下要用关键字final标记这个类

public final class Executive extends Manager{
	...
}

在这里的例子中,我们定义了一个类 Executive,他扩展了 Manager我想说不行不行
任何人都不能进一步扩展 Executive所以要加上 final

为什么想要这么做呢?有时对于系统类就是这样,在这种情况下,你希望绝对确保你定义的类就是人们使用的类而不能使用他的子类

例如 String类就是 final类

更常见的会把类的单个方法声明为 final

public class Employee{
    . . .
    public final String getName( ) { return name; }
}

在这里Employee 类选择将 getName 方法声明为 final

这样一来,任何人都不能覆盖这个 getName 方法所有人都清楚的知道要调用哪个 getName 方法
很早很早以前,有人说应该把所有方法都声明为 final因为这样更高效

理论上讲这样一来虚拟机就不用那么费劲的确定哪一个才是要调用的正确方法但这种说法已经不再成立,虚拟机非常擅长确定哪些方法不太可能被替代,他会推测假设大多数方法都是 final所以不要为了提高效率来使用关键字 final,而应当只是将它用于设计如果存在某种设计原因

要求一个方法绝对不能被覆盖,那当然就要把它声明为 final,否则不要加 final,有时对于一个特定的类型, 你对他的了解比编译器对他的了解还要多,这在处理很通用的类型时尤其常见这个课程后面就会介绍这个内容

例如:假设你知道 staff[0]尽管声明为一个 Employee,但他实际上是一个 Manager,现在你想在这个对象上调用一个 Manager方法

在这种情况下不能直接在一个 Employee 引用上调用 Manger方法因为编译器拒绝这样做实际上必须首先使用一个强制转换来转换类型

这仍然是第三课中见过的同样的强制类型转换技法当时用来将浮点数转换为整数

Manager boss = (Manager) staff[0];
boss.setBonus(...);

不过在这里并不是改变表示,你只是告诉编译器这个引用,还有他不知道的另一个不同的类型,所以在这里我们说尽管 staff[0]是一个 Employee但他实际上更是一个 Manager,现在他是一个 Manager 引用

可以把它放在一个真正的 Manager 中,此时我们就能在这个引用上,调用 Manager 方法了,如果你说了假话呢

在这种情况下你的程序运行时就会抛出一个 CastClassException异常第七课会介绍这个内容,这说明程序流程中断,这个错误的强制转换,不会有不好的影响,当然你肯定不希望你的程序中断

所以如果不能完全确定可以先做一个测试下面来看这样怎么做

if (staff[1] instanceof Manager){
	boss = (Manager) staff[1];
    ...
}

假设你想看看 staff[1] 是否也是一个 Manager

如果是, 你想做些处理但是如果 staff[1]不是一个 Manager你就不做这个处理,在这种情况下要使用 instanceof 操作符

这很简单,提供一个值,后面是关键字 instanceof然后是你想测试的类,这是一个布尔表达式 ,如果 staff[1]确实是一个 Manager , instanceof 操作符就会返回true ,否则他会返回 false

现在我们就很安全了,我们已经检查了staff [1]确实是 Manager,可以安全的完成强制类型转换
他不会抛出异常

顺便说一句如果 staff[1]null则不能通过 instanceof测试,编辑器非常了解类型系统,他会拒绝完全荒谬的强制类型转换,
例如 在这里我想把 staff[1]强制转换为一个 String,编译器知道 String 不是 Employee 的子类
所以这个强制转换绝对不可能成功,他会拒绝转换

使用继承时,我们会建立类的层次结构,更一般的类位于这个层次结构的上面,更特定的类比较靠下
Employee Manager 更一般 Manager Executive 更一般, 诸如此类,建立这些层次结构时, 你可能希望把公共行为抽取到上面, 例如如果有 Employee Student类他们有些公共的行为

比如说他们都有一个 name我们可能想说,这两个类有一个公共超类,也就是 Person,通过这个过程抽取出公共类时

这个层次结构上面的类可能会变得太过一般,很难为他们实现方法

例如假设 Employee类和 Student类都有一个 getDescription 方法
Employee 的描述是这样的,Student的描述却像下面这样

an employee with a salary of $50,000.00  	//薪水50000美元的雇员
a student majoring in computer science		//计算机专业的学生

现在我们想要在 Person定义 getDescription,从而能写一个循环迭代处理, 混合有 Employee Student的一个集合,并打印所有这些描述信息

不过 Person的描述是什么呢, 我们并不是很了解他没办法给出描述, 而且我们实际上也不认为系统中的任何对象只是一个 Person而已, 我们认为他们都是 Employee Student,或者是另外某种类型的 Person ,如果是这种情况 ,可以说在 Person层次上 ,getDescription 方法是抽象的,确实有这个方法 ,但是我们并没有提供这个方法的定义

 public abstract String getDescription( ) ;

为此只需要用关键字 abstract 标记这个方法,并在方法名后面加一个分号,而不是提供一个代码块
一个包含抽象方法的类,也必须声明为 abstract ,所以如果为 Person 增加了至少一个抽象方法, 就必须把整个类都定义为抽象类

public abstract class Person

这说明,我们的系统中实际上并没有任何 Person对象 Person只是一个抽象概念

不过他也不是完全抽象的,在一个抽象类中完全可以有一些字段构造器和具体方法,实际上这正是抽象类的全部目的

我们想把公共行为上移到超类中所以我们会说

没错尽管我们的系统中没有人仅仅是 Person ,但作为 Person的每一个人, 都会有一个名字,另外有些相关的方法来处理这个字段

public abstract class Person{
    private String name;
    public Person(String n) { name = n; }
    public String getName( ) { return name; }
    ...
}

会有一个构造器设置 name,另外还有一个 getName 方法来得到 name ,这些不是抽象方法,这些方法会做具体的工作

他们的目的是将由子类继承, 当然如果有一个抽象类就不能创建这个类的对象你不能实例化这个类

所以在这里

Person pl = new Person ( "Vince Vu");				// Error!
Person p2 = new Student( "Vince Vu", "Economics" ); // 0k

我试图提供一个名字来创建一个 Person对象这是不行的

另一方面我可以创建子类的一个对象,可以创建一个 Student对象,然后把它存储在一个 Person引用中 StudentPerson,所以这个赋值是完全合法的

如果你用某种机制, 适用于任何类型的 Person, 而不论他是 Student还是 Employee或者是任何其他类型的 Person就可以这么做

即使一个类中根本没有抽象方法, 把它声明为 abstract也是合法的,不过这不是很常见 ,如果你想保证任何人 ,要想创建这个类的实例 ,都必须首先扩展这个类 ,在这种情况下就可以这么做
在这个示例程序中

public abstract class Person {
    
    private String name;

    public abstract String getDescription();

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;

    }
}

可以看到我刚才介绍的 Person Employee student

首先来看 Person类 ,这是一个抽象类 ,它有一个抽象方法 ,也就是 getDescription(),这个方法没有具体实现,这里确实有一个具体字段 name 字段,一个设置这个字段的构造器,还有一个返回name的访问器, 这就是 Person类的全部内容

package com.horstmann.corejava.pojo;

import java.time.*;

public class Employee extends Person{
    
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        super(name);
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    @Override
    public String getDescription() {
        return String.format ( "an employee with a salary of $%.2f", salary) ;
    }
    
    public double getSalary() {
        return salary;
    }

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

Employee 是一个具体类 没有把它声明为 abstract 他扩展了 Person,这说明他必须为getDescription()方法提供一个实现,这里可以看到这个实现

注意这个类不再有一个name实例变量,这里只有对应工资salary和雇用日期hireDay的字段
name 定义在 Person 超类中

package com.horstmann.corejava.pojo;

public class Student extends Person{

    private String major;

    public Student(String name, String major) {
        super(name);
        this.major = major;
    }
    @Override
    public String getDescription() {
        return "a student majoring in " +major;
    }
}

Student 也是 Person 的一个字类,他也没有 name,也是一个具体类 ,他像这样定义getDescription

这里给出一个测试程序

 public static void main(String[] args) {
        Person[] people = new Person[2];
        // fill the people array with Student and Employee objects
        people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        people[1] = new Student("Maria Morris", "computer science");

        // print out names and descriptions of all Person objectsfor
        for (Person p : people) {
            System.out.println(p.getName() + ", " + p.getDescription());
        }
    }
//结果打印
Harry Hacker, an employee with a salary of $50000.00
Maria Morris, a student majoring in computer science

我们定义了一个 Person 类型的数组,在这个数组中, 放入两个不同类型的 Person
第一个是 Employee
第二个是 Student
因为 EmployeeStudent 都是 Person可以将 Employee引用转换为一个 Person引用, 也可以将 Student 引用转换为一个 Person引用

这里循环处理这个数组中的元素,打印他们的名字和描述 ,运行这个程序时,会得到我们期望的结果
就是这个 EmployeeStudent 的描述

注意在 p.getDescription 调用中 ,看起来我们好像在调用一个未定义的方法,回忆一下,我们并没有为 Person 定义 getDescription,p 是一个 Person,对于 Person,p.getDescription 实际上是未定义的,但是对于真正存在的所有类型的 Person也就是 Student Employee都定义了这个方法这里又是在使用多态

通过动态绑定过程 p.getDescription,会调用 Employee.getDescription或者 Student.getDescription

你已经见过类的特性可以是公共的私有的或者包可见的,继承为我们提供了第四个可见属性,称为 protected,即受保护的

public class Employee{
	protected double salary;
}

声明一个字段为 protected的时,这个字段对所有子类都可见,这意味着任何子类的任何方法,都能访问这个字段

例如如果你把 salary 字段声明为 protected Manager类就能访问这个字段,这样我们就不用在 ManagergetSalary 方法实现中调用 super.getSalary 了,听起来这是一个好处,但是当然这也有些危险,如果声明一个字段为 protected,那么不论子类来自哪里,每一个子类中的每一个方法都有权
访问这个字段,这就意味着你无法再删除这个字段,因为毕竟任何人都有可能扩展任何类, 除非那个类是 final 受保护的字段已经成为类合约的一部分

所以你可能不想对字段使用 protected ,有时受保护的方法更有用 ,看到一个 protected 的方法时,
这可能说明这个方法使用有些困难, 你并不希望一个类的所有用户都能使用这个方法 ,但他确实能做一些有用的工作 ,能提供子类中需要有的一些功能

4.0使用Object类及其方法

在java中每一个类都要扩展 object 类,或者换句话说 object java中每一个类的超类, 除了基本类型值以外,java中的所有一切都是对象

java.lang.object类是所有类的公共最高父类

甚至数组也是对象,可以把任何对象引用或数组引用存储在类型为 object的变量中

0bject obj1 = new Employee(...);
object obj2 = new int[10];

例如 在这里有一个变量 obj1 ,他的类型是 object , 可以在这个变量中放入一个 Employee对象
这里有另一个变量 obj2 ,我要放入一个数组引用,这是完全合法的

唯一不能放在 object 中的是整数 double,或者其他基本类型值
object 类提供了几个很有用的方法,具体包括方法 equals hashCode toString

equals 方法

equals()方法的目的,是定义两个对象彼此是否相等,按什么标准是相等的呢,这完全取决于你的类
想要如何比较对象,不同的类会有不同的标准

object.equals 不是抽象方法,这个方法实现为,会测试两个对象引用是否相同,换句话说object.equals 就是测试两个对象,是不是内存中完全相同的对象, 对于你的类来说,这可能是合适的
如果是这种情况,你可以直接使用而无需调整

另一方面,通常你可能希望对相等性测试, 有不同的理解 ,例如 如果两个 String有相同的内容,就认为他们是相等的

//这里的equals是在Employee中的
public boolean equals(Object otherObject){
        if (this == otherObject) {
            return true;
        }
        if (otherObject == null) {
            return false;
        }
        if (getClass() != otherObject.getClass()) {return false;}
        Employee other = (Employee) otherObject;
        return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay);
    }

下面对 Employee做同样的处理,如果两个 Employee,对象的所有字段都相等,我们就会说这两个 Employee对象是相等的

对于实际中的 Employee对象,可能是这样也可能不是这样,在现实中,你可能只想测试两个 Employee对象的 id, 如果他们的 id 相同,就认为这两个 Employee对象相等

不过这里还是考虑比较所有字段,因为我们就是想了解实现 equals 的机制, 这是典型的 equals 方法
注意参数的类型, 必须是 object ,覆盖方法时不允许改变参数类型

这有些不方便,因为我们不是想让 Employeeobject 比较,我们希望将 Employee与其他 Employee比较 怎么处理呢 可以看到这行代码我调用了 this也就是当前 Employee对象的 getClass() 方法,还调用了 otherObjecttgetClass() 方法 otherObject 是传入的任何对象,后面会看到 getClass() 会返回一个指示符,指出一个对象的类

如果这两个类不匹配就要返回 false ,这样一来, 一个员工与一个字符串 ,或者一个西瓜比较时
就会失败会指出他们不一样, equals 方法还有一个要求, 如果要与任何null值比较, 不能抛出一个异常, 而需要返回 false

这里的第二行代码就是要实现这一点, 注意我把这行代码放在前面, 因为在第三行代码中,我要在 otherObjects 上调用 getClass() 方法, 所以这个对象不能为 null

这里的第一行代码是为了提高效率, 有一个经常发生的实际情况, 有人会调用 equals将一个值与内存中的同一个对象进行比较, 如果他们是内存中完全相同的对象, 那么当然所有字段也是相同的, 所以这种情况下也要返回true

所以前面的这几行代码,是要处理各种特殊情况,下面来处理一般情况, 现在我们知道 otherObject,肯定是一个 Employee
因为这个对象的类与当前对象的类是一样的,所以可以安全的把它强制转换为一个Employee ,这个强制转换绝对不会失败, 现在需要检查他们的名字是否相同, 工资是否相同, 雇佣日期是否相同这样就能返回一个布尔表达式

当所有这三个字段都相同时,这个表达是为true,否则如果其用任何一个字段不同,这个表达式就为 false
换句话说,我可以把三个不同的测试用与操作&&的符号连起来, 第一个测试检查name是否相等
name是一个 String ,salary是 浮点数所以使用两个等于号来完成比较,第三个测试检查 hierDay 是否相同, 这里同样使用了这个类的 equals 方法,这个字段的类似 LocalDate

实际上对于第一个和第三个测试,还有一种更好一点的方法,因为有可能namehierDay null
在这种情况下,这个方法就会抛出一个异常,因为不允许在一个 null引用上调用任何方法,包括 equals

return 0bjects.equals(name,other.name)
&& salary == other.salary
&& 0bject.equals(hireDay,other.hireDay);

有一个静态object.equals 方法,会查找辅助工具类对象来处理这种情况, 调用 object.equals 比较其他引用时, 首先会检查是否其中一个引用为 null, 如果是那么两个引用都必须是 null

否则 Object.sequals 就要返回 false, 如果他们都不为 null,就直接调用常规的 equals 方法,所以要实现你自己的 equals 方法, 而且想要比较多个字段时, 如果这些字段是对象引用, 就把他们传递到 objects.equals 如果是基本类型就使用双等号, 所以实现这样一个 equals 方法相当简单

不过处理子类的情况就要复杂一些了

public class Manager extends Employee{
    . . .
    public boolean equals(0bject other0bject){
        if (!super.equals(otherObject)){ return false;}
        Manager other = (Manager) otherobject;
        return bonus == other.bonus;
    }
}

假设我们已经为 Employee ,实现了 equals 方法, 现在想为 Manager做同样的事情,这里还是使用同样的相等性概念, 我们说如果两个 Manager对象的所有字段都相等, 这两个 Manager对象就彼此相等, 首先要检查超类的 equals

记住我们不能访问超类的私有字段,所以需要通过一个公共接口来访问,这正是超类的 equals() 方法
所以我们要检查对象的超类部分, 是否彼此相等不过我们再来看看这个超类方法

因为这里有一点技巧,注意这里,在if (getClass() != otherObject.getClass()) {return false;}中我们要检查两个类是否匹配, 现在比较两个 Manager时, getClass 会返回正确的类
然后第一个 getClass() 调用和第二个 getClass() 调用, 会返回 Manager

这说明如果 super.equalstrue,就会继续执行这个子类方法,我们知道 otherObject 必然是一个 Manager,可以把它强制转换为 Manager, 然后比较 Manager的其余字段

现在比较棘手的问题是, 一个 Employee能不能等于一个 Manager 在这种情况下,我们的答案好像是不能 ,不过有些人会尝试实现适用于混合类型的相等性概念

你会在网上找到这样的一些例子,有些人实现了 equals 方法来比较点和有色点,诸如此类但是这很困难,之所以很难,是因为 equals 方法的规则限制很多,因为这些规则是java语言标准定义的
equals 方法需要具有一些基本性质:

  • 他应当是自反的 这表示任何值 x 与自身比较都应该为true
  • 他应当是对称的 如果 xy 相等, 也就是如果 x 等于 y , 那么 y 也要等于 x
  • 他应当是传递的, 如果 x 等于 y , 而且 y 等于 z , 那么 x 必然也等于 z

这些是基本性质, 所有 equals 实现都需要满足这些性质, 不过对于混合类型这会非常困难
如果 m 是一个 Manager, 对称性告诉我们 ,m.equals(e)e.equals(m) 的结果必须是一样的,但一点 equals.(m), 调用的是超类的 equals 方法

]这实际上表示 Manager, 关于相等性所做的任何决定,都必须与超类做出的决定完全相同, 如果你的情况就是如此 那当然很好

例如在一个 Person 类中可能如下定义了 equals

public final boolean equals(Object otherObject) {
        if (this == otherObject) {
            return true;
        }
        if (otherObject == null) {
            return false;
        }
        if (!(otherObject instanceof Person)) {
            return false;
        }
        Person other = (Person) otherObject;
        return id == other.id;
    }

我们说两个 Person相等, 当且仅当他们的 id相等,这是相等性的一个合理定义,现在 Person 可能有 EmployeeStudent 等子类

比较任意类型的 Person 时,根本不会查看子类, 只会查看 id, 如果你想完成混合类型的比较,而且这个比较, 在一个公共的超类中完成, 在这种情况下, equals 的代码要稍有修改, 这里不再比较类,而是要做一个 instanceof 测试

在这里我们会检查传入的对象,是否是一个 Person,这里没有调用 getClass()方法,而是使用了一个 instanceof 测试, 这个 equals 方法允许 Person与其他类型的 Person比较,而不论他们的类是否相同

所以在这种情况下instanceof 测试是合适的, 这里有一个经验 使用这种 instanceof 测试时 说明你想冻结 equals 的语义, 所以可能要把你的 equals 方法声明为 final

hashCode

object 类中定义的下一个方法名为 hashCode 有些数据结构, 比如散列表非常依赖这个方法
散列码是由一个对象得出的一个整数, 他有两个性质,我们希望散列码是杂乱的没有规律的,如果两个对象不相等,那么很可能他们的散列码也不相同

例如 String 类利用这里的这个公式计算散列码

int hash = 0;
for (int i = 0; i< length(); i++){
   hash = 31 * hash + charAt(i); 
}

他取各个字符的值,把他们加起来, 不过有点变化 , 每一步他会得到前一个总和, 将它乘以31, 然后再加上这个新字符, 这样就能很好的打乱编码
例如 hello 的散列码是这个数"Hello".hashCode() is 69609650,``Harry 的散列码是完全不同的一个数, 当然不同的字符串也可能恰好有相同的散列码

不过这种散列冲突很少见,所以我们希望散列码是打乱的, 但还有一个更紧要的条件, 就是散列码必须是一致的, 如果有两个对象彼此相等, 那他们的散列码也必须相等, 原因在于前面我说过的数据结构
也就是散列表 ,散列表会维护一组桶, 有相同散列码的对象会收集到一个桶里, 想要查找一个对象时, 只需要查看有指定散列码的那一个桶, 所以相等的对象绝对必须落在同一个桶中, 否则散列表会给出错误的信息

public class Employee{
    ...
    public int hashCode(){
        return 0bjects.hash(name,salary,hireDay);
    }
}

正如 object类定义了 equals 方法一样,他还定义了一个 hashCode 的方法, 这个方法与他自己的 equals 方法是一致的

应该记得, 如果两个对象的内存位置相同, 这两个对象就是相等的, 所以散列码, 要以某种方式由内存位置得出

覆盖 equals方法时, 还必须同时覆盖 hashCode的方法 ,这非常容易在我们的例子中, 我们覆盖了 Employeeequals方法而要比较所有实例字段

然后要做的就是定义 hashCode 的方法, 使他根据所有实例字段计算散列码, 为此有一个很方便的方法, 名为 objects.hash

同样的, 这个方法在 objects 工具类中定义, 这个方法可以接受可变数目的参数, 只需要把你在相等性比较中用到的所有实例字段, 都传入这个方法, 然后 objects.hash,会计算这个字符串的散列码

这个浮点数的散列码, 还有雇用日期的散列码, 以某种方式将他们打乱,

toString

你想为你自己的类定义的最后一个方法名为 toString, toString 就是要以类的创建者, 认为方便的某种方式, 返回一个对象的字符串表示, 实际上你已经见过 toString的使用

"Center: " +p // Calls p.toString()

只要一个字符串, 要与另外某个对象拼接, 就会在这个对象上调用 toString, 然后拼接所得到的字符串啊, object类定义了 toString, 来生成一个字符串包括类名 , 后面是省略码, 这对于调试用处不大

你通常希望覆盖 toString, 来得到一个更有意义的结果
例如java.awt.point 表示二维平面中的一个点 point 类使用以下表示:

java.awt.Point[x=10,y=20]

类名后面是大括号[]其中设置了一组实例字段,为此 point 类是这样实现的

public class Point{
    . . .
    public String toString(){
    	return "java.awt.point[x=" +X+ ",y=" + y + "]";
    }
}

toString 返回一个 String , 这个 String 包括类名, 然后是大括号, 字段名 字段值把所有这些放在一起, 这做起来很简单, 不过确实必须由你自己来提供这个实现, 不会自动的得到这个实现, 相比于上一页上看到的实现, 你还可以做的更好一些

public String toString(){
    return getclass( ).getName() + "[name=" + name + " ,salary=" + salary + " ,hireDay=" + hireDay + "]";
}

不是固定的写入类名, 你可以通过调用 getClass().getName()来得到类名, 这会得到一个类名字符串, 在他后面加上大括号[] 以及实例字段名和实例字段值, 这么做的好处是, 现在这个 toString 方法也适用于子类

如果在一个 Manager上调用 toString()然后会调用 getClass().getName() , 就会返回 Manager ,而不是 Employee所以他会正确的报告这个类的类型

当然在 Manager中你还想显示子类的字段这很容易做到

public String tostring(){
	return super.tostring( ) + " [bonus=" + bonus + "]";
}

Manager可以覆盖 toString, 首先调用 super.toString(), 然后加上子类的信息, 如果这么做
结果类似这样,

Manager[name=.. . ,salary=. . . ,hireDay=. . . ] [bonus=...]

首先是类名, 这是 getClass().getName()报告的, 后面是超类的字段, 然后是子类的字段, 注意他们在不同的中括号[]里, 这确实很好

这样一来你就能看出他们来自哪里, 另外要把所有这些字段都放在一组中括号里也很费劲, 因为超类提供了匹配的中括号

package com.horstmann.corejava.pojo;
import java.time.*;
import java.util.Objects;

public class Employee extends Person{

    private double salary;
    public LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        super(name);
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }
    @Override
    public String getDescription() {
        return String.format ( "an employee with a salary of $%.2f", salary) ;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary += raise;
    }
    @Override
    public boolean equals(Object otherObject){
        if (this == otherObject) {
            return true;
        }
        if (otherObject == null) {
            return false;
        }
        if (getClass() != otherObject.getClass()) {
            return false;
        }
        Employee other = (Employee) otherObject;
        return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay);
    }
    public int hashcode( ){
        return Objects.hash(name,salary,hireDay);
    }

    @Override
    public String toString() {
        return getClass().getName() + "salary=" + salary +", hireDay=" + hireDay + ", name='" + name + '\'' +'}';
    }
}

这个示例程序显示了 Employee 类和 Manager类, 其中都增加了 equals, hashCode toString 方法与你在前面看到的是一样的

Manager类中 ,我们覆盖了 equals ``hashCode toString

public class Manager extends Employee {

    private double bonus = 0;
    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        bonus = 0;
    }
    @Override
    public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }  
    @Override
    public boolean equals(Object other0bject) {
        if (!super.equals(other0bject)) {
            return false;
        }
        Manager other = (Manager) other0bject;
        // super.equals checked that this and other belong to the same class
        return bonus == other.bonus;
    }
    @Override
    public int hashcode() {
        return super.hashcode() + 17 * new Double(bonus).hashCode();
    }

    @Override
    public String toString() {
        return super.toString() + " [bonus=" + bonus + "]" ;
    }
}

注意 Manager.equals 方法会调用 Employeeequals 方法, 只有当 Employee部分相同时
我们才会继续比较 Manager部分此类的 hashCode的方法会调用超类的 hashCode,然后增加 bonus 的散列码 , toString 方法调用超类的 toString, 并增加 bonus 字段

在测试程序中,

public static void main(String[] args) {
        Employee alice1 = new Employee("Alice Adams ", 75000, 1987, 12, 15);
        Employee alice2 = alice1;
        Employee alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
        Employee bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
        System.out.println("alice1 == alice2: " + (alice1 == alice2));
        System.out.println("alice1 == alice3: " + (alice1 == alice3));
        System.out.println("alice1.equals(alice3): " + alice1.equals(alice3));
        System.out.println("alice1.equals(bob): " + alice1.equals(bob));
        System.out.println("bob.toString(): " + bob);

        Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        Manager boss = new Manager("carl cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        System.out.println("boss.toString(): " + boss);
        System.out.println("carl.equals(boss): " + carl.equals(boss));
        System.out.println("alice1.hashCode(): " + alice1.hashCode());
        System.out.println("alice3.hashCode(): " + alice3.hashCode());
        System.out.println("bob.hashcode(): " + bob.hashCode());
        System.out.println("carl.hashCode(): " + carl.hashcode());
}
输出:
alice1 == alice2: true
alice1 == alice3: false
alice1.equals(alice3): false
alice1.equals(bob): false
bob.toString(): com.horstmann.corejava.pojo.Employeesalary=50000.0, hireDay=1989-10-01, name='Bob Brandson'}
boss.toString(): com.horstmann.corejava.pojo.Managersalary=80000.0, hireDay=1987-12-15, name='carl cracker'} [bonus=5000.0]
carl.equals(boss): false
alice1.hashCode(): 356573597
alice3.hashCode(): 1735600054
bob.hashcode(): 21685669
carl.hashCode(): -341762419

我们建立了一组不同的 EmployeeManager , 在这里建立两个 Employee, 为他们提供相同的引用,然后创建另一个 Employee, 其中包含相同的信息, 不过他存储在一个不同的对象中

最后再创建一个完全不同的 Employee, 我们会得到什么? ,我们希望由于 alice1alice2是相同的, 所以使用双等号操作符时会得到 true 当然, 调用 equals 方法时应该也会得到 true

另一方面 比较两个包含相同内容, 但在不同对象中的 alice时, 也就是 alice1alice3, ,双等号应该返回 false, 但 equals 方法应该返回true

在这里可以看到利用 toString, 和 hashCode 做的其他计算 你要自己运行这个程序并研究他的输出


5.0继承如何支持java语言特性

继承是一个重要的特性他还决定了java中其他一些特性如何实现

例如 java有一个名为 ArrayList 的类,这个类可以克服数组的缺点

大家应该记得,创建一个数组时数组的长度是固定的,这很不方便, 因为通常你并不知道, 要在一个数组中放多少个元素 , ArrayList 的做法是, 他会管理一个可以根据需要扩大和收缩的数组
是什么类型的数组呢?

为了尽可能灵活, ArrayList 类会管理一个 Object 数组, 所以在一个 ArrayList 类中, 实际上有一个类型为 Object[]的数组 , 现在为了能很容易的使用 ArrayList 类, 从java5开始ArrayList 改为有一个所谓的类型参数

这样一来建立一个 ArrayList 时, 需要指定你想存储的元素的类型 , 例如如果你想存储 Employee , 就要声明一个 ArrayList<Employee> , 如果想存储 String,就要声明一个 ArrayList<String>

java使用这种信息,, 来保证当你查找元素时可以得到正确类型的元素, 如果你要存储元素不会意外的放入错误类型的元素 , 在较早的java版本中并不是这样 , 实际上你要处理存储原始 Object ArrayList

建立一个 ArrayList 时你可能想把它放在某个变量中

ArrayList<Employee> staff = new ArrayList<>();

在这里我们建立了一个 EmployeeArrayList,我们把这个变量命名为 staff,要注意与java中的其他变量声明一样 , 还是先有类型然后是变量名 , 初始化这个变量时 你会构造一个 ArrayList, 为了减少代码中的重复, 构造时允许省略类型参数, 仍然必须加上尖括号, 不过不用重复写 Employee

因为java编译器很聪明 , 能够确定如果这个变量是一个 Employee 数组 , 而你现在正在构造一个 ArrayList
那么这也必须是一个 Employee ArrayList ;

这一对空尖括号看起来就像是一个菱形, 有些人就把这称为菱形语法

staff.add(new Employee( "Harry Hacker" , . . .));

现在 ArrayList 是一个类, 所以他有方法, 可以使用 add 的方法, 在末尾增加一个对象, 正是这个 add 的方法使得 ArrayList 如此方便, 你可以增加想要的任意多个对象, 内部数组会根据需要扩大, 如果这个内部数组不够大 , 就会分配一个新的数组, 所有的值会移到新数组中 ArrayList对象会保留这个新数组,
这样一来你就完全不用担心空间用尽了

获取ArrayList长度

要得出目前 ArrayList 中有多少个元素, 可以调用 size() 方法

Employee e = staff.get(i);
staff.set(i, tony);

get set 方法可以用来访问 第I个元素, 以及更新第I元素
对于数组可以使用中括号操作符, 但 ArrayList 是类 , 所以不能使用中括号

for (Employee e : staff){ 
	System.out.println(e);
}

另一方面如果要访问一个 ArrayList 中的所有元素则与数组完全一样, 可以使用 for each循环来访问所有元素 , 这个 for 循环中间有一个冒号:, 冒号后面是集合名, 还有一个对应元素的变量, 执行循环时
这个变量会轮流设置为这个集合中的每一个元素, 所以在这个循环中 , 我们会逐个处理所有元素 e
这里只是打印所有这些元素

在这个简单的测试程序中

public static void main(String[] args) {
        // fill the staff array list with three Employee object:
        // 用三个Employee对象填充staff数组列表:
        ArrayList<Employee> staff = new ArrayList<>( );
        staff.add (new Employee( "carl Cracker",75000,1987,12,15));
        staff.add (new Employee("Harry Hacker",50000,1989,10,1));
        staff.add (new Employee( "Tony Tester",40000,1990,3,15));
        //raise everyone's salary by 5%     把每个人的工资提高5%
        for (Employee e : staff) {
            e.raiseSalary(5);
        }
        //print out information about all Employee objects  打印所有员工对象的相关信息
        for (Employee e: staff){
            System.out.println( " name=" + e.getName( ) + " ,salary=" + e.getSalary() +",hireDay:"+e.getHireDay());
        }
    }
 打印:
 name=carl Cracker ,salary=78750.0,hireDay:1987-12-15
 name=Harry Hacker ,salary=52500.0,hireDay:1989-10-01
 name=Tony  Tester ,salary=42000.0,hireDay:1990-03-15

我声明了一个 Employee ArrayList与上一页看到的一样 用 add 在这个 ArrayList 中增加三个 Employee ,然后有一个循环 , 这会遍历所有 Employee , 在这些 Employee上调用一个方法 , 可以看到
处理 ArrayList 确实非常简单 , 不过 ArrayList 有一个限制 ,这就是 ArrayList 只能保存对象 ,而不能保存基本类型的值 , 比如 int

所以如果你想利用 ArrayList的方便性, 也就是能够根据需要扩大和收缩 , 不过还想存储 整数 或者浮点值可能就有些不知所措了 , 由于这个原因 , java为所有基本类型提供了包装器类 ,

ArrayList<Integer> list = new ArrayList<>();
list.add(3);			// same as list.add(Integer.value0f(3));
int n = list.get(i);	// same as int n = list.get(i).intValue() ;

有一个包装器 Integer, 可以保存 int 类型的值 , Integer 类型的值是一个对象 , 因为它是一个类的实例 , 因此可以在 ArrayList 中收集 Integer 对象 ,这里就是这么做的

我有一个列表 list , 他的类型是一个 Integer ArrayList , 如果我写为 int ArrayList, 小写的 int , 编译器会标记这是一个错误 , 现在为了更为方便java编译器 , 会在 int 值和 Integer 对象之间完成转换 , 转换完全是自动完成的 , 所以如果我调用 list.add(3) , 这里的3当然是一个 int 值 , 但是这本应该是一个 Integer 对象 , 编译器会自动的将3转换为一个 Integer

相反调用 get 方法时 , 我得到的是一个 Integer 对象 , 而不是一个 int 值 ,

Integer n = 1000; n++;

但是这里我把它赋给一个 int 值 , 编译器会插入这个抽取调用 , 从这个 Integer 中得到 int, 这称为装箱和拆箱 ,可能更适合的术语应该是包装和解包

不过不管怎样 , 出于某种原因这被叫做装箱, 装箱甚至也适用于自增 , 所以如果我有一个 Integer他的值为1,000, 现在我调用 n++

当然了这个++,实际上并不是在一个对象上操作, 而是会将这个对象拆箱 , 让整数自增 , 然后再将结果装箱放回

这里只有一点必须担心于 Integer对象==操作符不能正确工作 ,

Integer a = n + l;
Integer b = n + l;
System.out.println(a == b); 	// May be false

在这里我有两个 Integer对象 , 他们都设置为 n + 1 , 你可能认为他们当然是相等的 , 但==会检查他们是否是相同的对象, 但他们实际上存储在不同的内存位置上 , 所以在这个特定情况下==会报告 false
补救方法是想要比较 Integer 时, 只需要使用 equals 方法 , 就像比较 String 一样

Integer n = null;
System.out.println(n + 1);// Null pointer exception

Integer 在另一个方面也与 int 有区别, 这就是类似于所有其他对象引用,包装器可以为 null , 所以一个 Integer 变量完全有可能是 null , 而 int 不可能是 null

如果可能发生这种情况 , 你就不能使用 Integer , 例如 如果你试图为它加1会得到一个空指针异常
nollPointerException , 就像试图在 null引用上完成任何其他操作时一样

System.out.printf( "%d" ,n);
System.out.printf("%d %s", n,"widgets" );

你已经见过一些接受可变数目参数的方法如 printf() 大家应该记得,printf 接受一个格式字符串 , 作为他的第一个参数, 然后可以接受任意多个其他参数 , 他们当然要与格式字符串中的这些百分号匹配, ,不过到现在为止 , 你还不知道如何实现这样一个方法
下面就来告诉你

public class PrintStream{
	public PrintStream printf(String fmt,0bject.. . args) {...}
}

printf 声明如下 有一个 String类型的参数 , 然后有另一个参数 , 声明为 Object后面带三个点
...也就是省略号 这个参数名为 args , 这看起来好像只有一个参数

不过这三个点只是调用这个方法时 , 你可以提供你想要的任意多个参数 , 他们都必须是 Object 类型
这个要求不算什么, 因为这里的 String确实是 Object , 另外如果有一个整数n就像这里一样, 他也会装箱为一个 Integer 对象 , 所以 args会存放所有对象

实际上, 实现这个方法是这实际上就是一个数组, 这是一个类型为 Object[]的数组 , 它会包含包装后的基本类型值, 或者传入的真正的对象 , 他的长度取决于实际的方法调用

在第一个方法调用中这是一个长度为1的数组 , 在第二个方法调用中这是一个长度为2的数组
下面来试试我们自己的方法

public static double max(double. . . values){
    double largest = Double.NEGATIVE_INFINITY;
    for (double v : values){
        if (v > largest) largest = v;
    } 
    return largest;
}

这里给出的方法会计算一些浮点值的最大值 , 这个方法接收一个值数组 values在这里我要使用一个增强的 for 循环遍历这个数组 , 我要用通常的方法计算最大值 并返回这个值 , 这里的关键是. 实现这个方法时
我得到的是一个传入的数组 values , 而我调用这个方法时 ,可以在参数列表中提供任意多个值

double max( 3.2,40.4,-5);

这里调用 max 并提供三个值, 他们都会收集到一个数组中, 这个方法会处理这个数组 ,

大家应该记得第三课中介绍过 , java可以定义枚举类型他们也是类

public EnumSize { SMALL,MEDIUM,LARGE,EXTRA_LARGE };

定义一个枚举 size时 ,在这里可以看到他有四个可能的不同实例, 这确实是一个类 ,这四个实例会自动创建 , 如果愿意你还可以为枚举类添加字段和方法,

public enum size{
    SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE(""XL");
    private String abbreviation;
	private size(String abbreviation) {
        this.abbreviation = abbreviation; 
    }
    public String getAbbreviation( ) { return abbreviation; }
}

这里有一个字段 abbreviation, 所以这四个实例现在都有一个 abbreviation字段, 还有一个方法返回这个字段所以现在如果有一个Size实例, 可以在这个实例上调用 getAbbreviation

abbreviation 在构造器中设置, 这里可以看到构造器的代码 ,他得到一个 String并把这个 String设置到 this.abbreviation

要调用构造器换句话说 ,为了确保正确的设置这个字段 , 必须把构造参数 放在枚举常量名后面的小括号里
, 所以在这里我们说 size 是一个 enum 有四个实例 SMALLMEDIUMLARGEEXTRA_LARGE
构造 SMALL时提供一个 S , MEDIUM需要一个 M , LARGE需要一个 L , EXTRA_LARGE 需要一个 XL
这些字符串存储在 abbrvation 实例变量中,

只要定义一个枚举类, 不论采用简单的方式还是这种比较复杂的方式, 他们都是一个名为 Enum的类的子类 Enum超类有一些很有用的方法它有一个 toString 方法能自动生成正确的名

所以在 size.SMALL上调用 toString时他会返回 SMALL这很好, 因为手动指定名字有些枯燥,

还有一个方法名为 ordinal, 这会按定义枚举常量的顺序, 给出他们的位置所以在 size.SMALL上调用 ordinal 时会得到0, 在 size.MEDIUM上调用 ordinal 时会得到1 以此类推

另外Enum还提供了几个有用的静态方法

Enum.valueOf(Size.class,"SMALL") //提供 size.SMALL

调用 Enum.valueOf并提供一个字符串 SMALL时, 会得到 size.SMALL你要告诉他在哪个枚举类型中
搜索 SMALL, 可以看到这在第一个参数中指出, 这里写为 size.class这是一个类字面量稍后会介绍
不过这很容易使用, 只需要写出枚举名后面跟着.class, 然后指定你想生成的任何字符串, 当然这样做没有任何好处

但是有些程序中你可能想动态生成这些枚举值, 比如要从一个用户或者从一个文件读入, 就可以使用这种形式,
另外还有一个.value方法

Size.values() //生成tyoe Si数组中的所有值

可以得到一个枚举以及一个数组中的所有值, 从而可以遍历所有这些值, 所以可以看到枚举是类, 他们可以利用继承, 从而可以由公共的Enum超类提供基本的服务
这个示例程序:具体使用了前面看到的枚举

public class EnumTest {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in) ;
        System.out.print( "Enter a size: (SMALL,MEDIUM,LARGE,EXTRA_LARGE)");
        String input = in.next( ).toUpperCase( );
        Size size = Enum.valueOf(Size.class,input);
        System.out.println( "size=" +size);
        System.out.println( "abbreviation=" + size.getAbbreviation() );
        if (size == Size.EXTRA_LARGE) {
            System.out.println("Good job--you paid attention to the _.");
        }
    }

    enum Size{
        SMALL("S"),MEDIUM("M" ),LARGE( "L"),EXTRA_LARGE("XL");
        private Size(String abbreviation) { this.abbreviation = abbreviation; }
        public String getAbbreviation( ) { return abbreviation; }
        private String abbreviation;
    }
}
输出:
Enter a size: (SMALL,MEDIUM,LARGE,EXTRA_LARGE) extra_large
size=EXTRA_LARGE
abbreviation=XL
Good job--you paid attention to the _.

这里的代码与课件中看到的代码完全相同, 同样是一个有4个值的枚举, 这里我们要求用户键入一个大小
,可以是 SMALL, MEDIUM , LARGE,或EXTRA_LARGE, 用户提供的当然是一个字符串, 现在我想得到相应的 enum 值为此要使用 enum.valueOf方法, 这会得到一个 enum 值, 然后打印这个值, 并打印一个缩写

这里我要检查用户进入的是否是 extra_large, 因此我要得到 Size.EXTRA_LARGE, 如果是, 我会说不错
你注意到了下划线, 不过这里要注意的很重要的一点是, Size 是一个 Enum对象Size.EXTRA_LARGE 是一个 Enum对象, 我在用双等号比较这两个对象, 而没有使用 equals 方法, 对于 Enum, 这是完全合法的, 因为这个枚举只有这四个实例

所以如果 size Size.EXTRA_LARGE, 这与常量 Size.EXTRA_LARGE 完全是一样的, 不存在两个不同对象拥有相同值的情况, 所以在这种情况下使用双等号是合法的, 不用使用 equals

下面运行这个程序键入 extra_large, 他会转换为大写, 这里是 toString的输出

我在前面说过一个枚举的 toString, 会自动得出这个枚举的名称, 这里并没有看到 toString, 不过要记住, 如果有一个 String将他与任何东西拼接时, 另外那个东西就会转换为一个 String, 然后调用 getAbbreviation 方法, 这里会得到XL

6.0使用反射处理任意对象

能够分析类的程序称为反射

java中一个非常强大的特性称为反射

利用反射你可以在运行的程序中,分析和管理任意类型的对象我很喜欢反射,因为利用反射, 我能构建强大而且很通用的工具, 但必须确认反射有些复杂, 你可能从来都不需要利用反射来编程, 不过能知道底层是怎么做的总是好的, 这样你就能理解其他人, 如何构建这些强大的工具

下面开始介绍

Object obj = ...;
Class cl = obj.getclass();

如果有某个对象, 你可能会问这个对象你属于哪个类, 为此要调用 getClass()方法, 你得到的是一个类的对象, 这个类名为 class

刚开始听起来可能有些混乱, 不过 class只表示类的一个描述符, 所以你得到了这个特定类的一个描述符
对于这样一个类描述符你能做什么呢?

首先你可以直接用 getName询问这个类的名字

System.out.println(cl.getName());

我们不知道运行这个代码事会发生什么, 因为这完全取决于这个对象里有什么, 如果这是一个字符串, 我们就会得到 java.long.String, 如果这是一个 Employee , 我们就会得到 Employee ,

System.out.println(Arrays.toString(cl.getMethods()));

可以问这个类你有什么方法你会得到一个方法数组

最后还可以要求这个类

object new0bj = cl.newInstance();

嘿为我创建一个属于你的对象, 为我创建你的类的一个实例, 假设这个类有一个无参数的构造器, 就会调用那个构造器, 下面在JShell试试看

在这里我在检查这个字符串的类, 我得到了一个 class 对象可以向他询问这个类的名字gatName(), 他告诉我这是 java.lang.String或者你可能想知道 system.out属于哪个类 , 要知道是哪个类的实例我们可以直接问getClass, 会得出他是java.io.PrintStream 的一个实例

现在再回到 String类我们可以问他你有什么方法gatMethods()我们会得到一个方法数组, 不过这里有点让人失望, 如果你有一个数组, toString 是无法使用的

出于一些神秘的历史原因, 没有为数组定义 toString, 所以你会得到一个描述, 只是用一种非常奇怪的记法, 提供这个数组的类型: 中括号 L 元素类名以及一个分号 这是数组类型在虚拟机中的实现
奇怪的是这个实现会暴露在这里, 然后是散裂码, 这些都不是我们想知道的, 一般来讲如果有一个数组, 你可能希望把它看作是一个数组, 你想使用遍历方法 Array.toString, 这是 Array类的一个静态方法, 会用通常的方式列出数组的内容

实际上 String类有非常多的方法, 这里可以看到所有这些方法, 最后能创建 String 的一个新实例吗?是的当然能

String className = ...	 // e.g.,"java.util.Random" ;
class cl = class.forName(className);

我们会得到一个空串, 现在你已经了解了 ,得到 class 对象的一种方法, 就是取任何原有对象在这个对象上调用 getClass()

还有另外两种方法

Class cl1 = Random.class; // if you import java.util.*;
class cl2 = int.class;
Class cl3 = Double[].class;

首先如果有一个类名, 他是一个 String, 例如java.util.Random可以把它传递到 class.forName 方法, 这样就能得到这个类的类描述符
另外在一个java程序中, 你可能希望得到你很想了解的一个类的类描述符,可以在这里写出这个类的类名, 比如 Random然后加上.class, 这样就能得到一个类字面量

注意 在这种情况下如果已经导入了java.util.*, 你甚至不用完整的写出java.util.Random Random 能正确解析 , Random.class 会为你返回类字面量这也适用于基本类型

进入 int.class 时,可以得到 int 类型的描述符, 如果写为 double[].class, 就会得到这个数组类型的描述符, 所以换句话说 class 这个名字有点不太合适, 因为 class 可以描述任意类型, 这可能是一个类也可能不是, , 没错 Random.class 描述一个真正的类

double[].class 也是, 因为数组也是类, 但是 int 不是类, 甚至有一个 void.class, 所以 class 描述的是一个类, 这可能是也可能不是类,

if (obj.getclass() == Employee.class)// 0k

另外 class 实例是唯一的, 所以在虚拟机中, Employee 类只有一个描述符, 可以用 Employee.class 引用, 或者可以在一个 Employee 对象上,调用 getClass()来得到, 可以安全的使用==来比较这两个描述符, 因为类描述符是唯一的, 使用类编程时必须考虑异常, 就如 forName getnewInstance 等方法可能会抛出异常

如果你试图得到一个不存在的类的 class 对象forName 就会抛出一个异常, 如果你想构造一个实例,但这个类没有无参数构造器, newInstance 就会抛出一个异常

与目前为止见过的大多数异常不同, 这些异常是检查型异常, 你要告诉编译器如何处理这些异常
可以用一个 throws 语句声明这些异常

try{
	statements that might throw exceptions}
catch (Exception e){
	handler action
}

我们在第三课中, 对于输入输出异常就是这样做的, 或者可以用一个所谓的, try catch块捕获这些异常
, 这里可以看到,把可能抛出异常的语句包围在一个try块中然后增加一个 catch子句, 其中有一个处理器,如果这个异常出现就会触发这个处理器

在示例程序中我们就将采用这种方法, 下面来看这个示例程序

package com.horstmann.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

import static java.lang.Class.forName;

/**
 * @-*- coding = utf-8 -*-
 * @Time : 2022/4/8 11:22
 * #@Author : 彭传
 * #@File : RandomTest.java
 * #@Software :IntelliJ IDEA
 */
public class RandomTest {
    public static void main(String[] args) {

        //read class name from command line args user input
        String name;
        if (args.length > 0) {
            name = args[0];
        } else {
            Scanner in = new Scanner(System.in);
            System.out.println("Enter class name (e.g.java.util.Date):");
            name = in.next();
        }

        try {
            // print class name and superclass name (if!= object)
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }

            System.out.print("class " + name);
            if (supercl != null && supercl != Object.class) {
                System.out.print(" extends"+ supercl.getName());
            }
            System.out.print("\n{\n");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }
    /*
     *Prints all constructors of  a class    打印一个类的所有构造函数
     *@param cl a class
     */

    public static void printConstructors(Class cl) {
        Constructor[] constructors = cl.getDeclaredConstructors();

        for (Contractorc : constructors) {
            String name = c.getName();
            System.out.print("");
            String modifiers = Modifier.toString(c
                    .getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + "");
            }
            System.out.print(name + "(");
            // print parameter types
            Class[] paramTypes = c.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) {
                    System.out.print(",");
                }
                System.out.print(paramTypes[j].getName());
            }
        }
        System.out.println(");");
    }


    /*
     *Prints all methods of a class 打印一个类的所有方法
     *@param cl a class
     */
    public static void printMethods(Class cl) {
        Method[] methods = cl.getDeclaredMethods();
        for (Method m : methods) {

            Class retType = m.getReturnType();
            String name = m.getName();
            System.out.print("");
            // print modifiers, return type and method name
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(retType.getName() + " " + name + "(");
            // print parameter types
            Class[] paramTypes = m.getParameterTypes();
            for (int j = 0; j < paramTypes.length; j++) {
                if (j > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[j].getName());
            }
            System.out.println(");");
        }
    }
    /*
     *Prints all fields of  a class     打印一个类的所有字段
     *@param cl a class
     */
    public static void printFields(Class cl){
        Field[] fields = cl.getFields();
        for (Field field : fields) {
            Class type = field.getType();
            String name = field.getName();
            System.out.println(" ");
            String modifiers = Modifier.toString(field.getModifiers());
            if (modifiers.length() > 0) {
                System.out.println("modifiers" +"  ");
            }
            System.out.println(type.getName() +" "+name+";");
        }
    }
}

这个程序允许用户建入一个类的名字然后, 他会得到对应这个名字的 class 对象这里会在这个 class 对象上, 调用很多方法, 我们要得到超类,这是另一个 class 对象 , 我们要得到修饰符, 就是 public 之类的修饰符

还有一个方法, 把他们格式化为一个字符串, 然后继续打印所有构造器方法和字段
稍后我会介绍这些工具方法的代码

还要注意, 我把所有这些活动都包围在一个, try catch 块中, 这样一来如果有任何异常, 就会报告相应异常, 为了打印构造器必须向这个 class 询问他的构造器, 有一个名为 getDeclaredConstructors 的方法

这个方法会提供这一级声明的所有构造器, 而不包括超类一级声明的构造器, 我会得到一个 Contractor对象的数组, Contractor类本身,有一些方法可以得到修饰符, 当然也可能让公共或私有构造器得到参数类型, 并打印所有参数类型, 对于方法可以得到参数类型和返回类型

具体细节并不重要, 因为大多数程序员, 都不需要那么深入的了解反射的构造器或方法, 不过能够在运行时得到所有这些信息很有意思, , 下面就来看看这些信息, 现在键入一个任意的类名,我们要键入java.util.Random
进入后我会得到这个输出

输入: java.util.Random
输出:     
public class java.util.Random
{
public java.util.Random(publicjava.util.Random(long);
protected int next(int);
private void readObject(java.io.ObjectInputStream);
private synchronized void writeObject(java.io.ObjectOutputStream);
public int nextInt(int);
public int nextInt();
public double nextDouble();
public boolean nextBoolean();
public float nextFloat();
public long nextLong();
public java.util.stream.DoubleStream doubles(long, double, double);
public java.util.stream.DoubleStream doubles(long);
public java.util.stream.DoubleStream doubles();
public java.util.stream.DoubleStream doubles(double, double);
private static long initialScramble(long);
final double internalNextDouble(double, double);
final int internalNextInt(int, int);
final long internalNextLong(long, long);
public java.util.stream.IntStream ints(int, int);
public java.util.stream.IntStream ints(long, int, int);
public java.util.stream.IntStream ints();
public java.util.stream.IntStream ints(long);
public java.util.stream.LongStream longs(long);
public java.util.stream.LongStream longs();
public java.util.stream.LongStream longs(long, long, long);
public java.util.stream.LongStream longs(long, long);
public void nextBytes([B);
public synchronized double nextGaussian();
private void resetSeed(long);
private static long seedUniquifier();
public synchronized void setSeed(long);
}

注意这个输出完全是自动生成的, 程序并不知道我要进入java.util.Random, 所以他必须查找这个类的所有信息, 可以看到所有方法的一个列表, 包括返回类型和参数类型, 这里还列出了所有字段

我不知道所有这些都是随机术生成器中的内容, 不过这下就知道了, 利用反射能做的一件事, 就是在运行时分析对象的内容, 甚至可以修改对象

下面来介绍这要怎么做

Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getclass();
Field f = cl.getDeclaredField( "name");
0bject v = f.get(harry) ;
f.set(harry,"Wimpy whiner");

要使用 class 对象, 然后调用 getDeclaredFields, 得到所有字段描述符的一个数组或者调用 getDeclaredField并提供一个特定字段的字段名, 就会得到指定字段的字段描述符, 然后可以用它来检查或修改字段值, 在这里有一个 Employee 首先得到的 class, 让这个 class 提供 name字段的字段描述符, 然后使用这个字段描述符, 在这里我说, 把对象 harry name 字段给我, f 知道他要负责name字段, 当他得到一个正确的类的对象时, 就会在运行时得出这个值是什么, get 的返回类型是 Object
因为他可以是任意的对象, 如果字段类型是一个类类型, 你会得到这个对象, 或者如果字段类型是一个基本类型, 就会把它包装为对象, 让人更惊奇的是, 不仅能查看任意的对象, 甚至还可以在运行时修改任意的对象. 所以如果使用 set 方法, 我可以得到 harry 对象的name字段, 把这个 name 设置为其他名字, 为了完成这个设置, 显然这看起来有些危险
必须多做一步

f.setAccessible(true);

在字段描述符上调用 setAccessible(true), 如果运行程序时有一个安全管理器, 安全管理器可能配置为禁止这个访问, 因为显然不是所有程序, 都允许修改任意对象的任意字段, 这种技术的一种应用就是, 写一个适用于所有对象的通用 toString方法

下面就来介绍这要怎么做

private ArrayList<Object> visited = new ArrayList<>();

    /**
     * Converts an object to a string representation that lists all fields.
     * @param obj an object
     * @return a string with the object's class name and all field names and
     * values
     */
    public String toString(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (visited.contains(obj)) {
            return "...";
        }
        visited.add(obj);
        Class cl = obj.getClass();
        if (cl == String.class) {
            return (String) obj;
        }
        if (cl.isArray()) {
            String r = cl.getComponentType() + "[]{";
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {
                    r += ",";
                }
                Object val = Array.get(obj,i);
                if (cl.getComponentType().isPrimitive()) {
                    r += val;
                } else {
                    r += toString(val);
                }
            }
            return "}";
        }
        String r = cl.getName();
        // inspect the fields of this class and all superclasses
        //检查这个类和所有超类的字段
        do {
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);
            // get the names and values of all fields 获取所有字段的名称和值
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    if (!r.endsWith("[")) {
                        r += ",";
                    }
                    r += f.getName() + "=";
                    try {
                        Class t = f.getType();
                        Object val = f.get(obj);
                        if (t.isPrimitive()) {
                            r += val;
                        } else {
                            r += toString(val);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            cl = cl.getSuperclass();
        }
        while (cl != null);
        return r;
    }

在这里有一个名为 toString 的方法, 他接受一个任意的对象, 我们不知道这是什么类的对象, 这个方法会生成这个对象中所有字段的一个表示, 这里采用之前手动实现 , toString 方法时见过的方式来表示, 要做到这一点实际上并不容易 , 因为对象可能有环 , 假设我们要检查某个对象, 比如说是一个链表其中包含节点 , 有些节点有反向链接 , 这样一来 调用 toString 时就会出现一个无限循环, 了避免这种环, 要为已经见过的对象维护一个 ArrayList , 这里可以看到 , 如果发现可能要打印一个已经见过的对象, 就改为打印三个点 , 这个代码有些复杂

如果你想知道如何达到这种效果, 可以自己好好研究研究, 不过基本思想是这样的, 我们要得到这个对象的类, 如果这个类是 String 类, 就把这个对象作为一个 String 返回, 我们知道如何实现一个 String toString

如果这是一个数组 , 就是用 Array之类的方法得到这个数组的长度, 以及得到元素, 适当的加逗号把他们放在一起, 这才是我们所希望的显示数组的方式 , 而不是默认得到的那种奇怪的表示, 否则得到类名, 处理所有字段必须调用 setAccessible(ture)从而能查看字段

我们要得到修饰符, 并以某种方式格式化, 具体细节有点繁琐 ,这里得到字段值, 然后对字段值递归的应用我们的方法, 使他以格式化为一个字符串, 然后继续对超类做同样的处理 , 这里有一个循环, 会不断访问超类直到超类为null;这说明我们已经到达 Object

下面来看一个例子

public static void main(String[] args) {
        ArrayList<Integer> squares = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            squares.add(i * i);
        }
        System.out.println(new ObjectAnalyzer().toString(squares));
    }

这里我要建立一个 IntegeredArrayList填入前五个平方数 1491625
现在我让 ObjectAnalyzer 分析这个 ArrayList, 下面来看会发生什么
这是一个java.util.ArrayList, 元素数据是一个 Object 数组, 填入了一个 Integer, 值为1 看这里的包装器没错, Integer 是类, 在这里他有一个实例值, 然后是另一个java.lang.Integer, 他的值为4

如此继续, 这些都是值 ,这里可以看到一组 null所以可以看到, 这个 ArrayList 管理了一个 Object 数组,这个数组比我们实际需要的稍大一点, 这是 ArrayList size 字段, 这里还有另一个字段, 名为 modCount, 他会统计这个 ArrayList 被更改多少次, 我们已经查看了 ArrayList

如何组织他的内部数据, 这个代码完全独立于 ArrayList, 你可以使用这个示例程序, 查看你想了解的任何类, 反射包有一个类, 而愿意允许动态的创建和分析数组, 我会简单做一些介绍
因为即使你对这个这么做不感兴趣, 起码可以对java中的数组如何, 工作有些了解

Employee[] a = new Employee[100];
// array is full
a = Arrays.copyof(a,2*a.length);

假设我想实现一个 copyOf 方法要建立一个数组的副本, 而且要有指定的长度, 例如这里有一个 Employee 数组 , 已经填入了 Employee 对象, 然后我想调用 `copyOf使所得到的数组长度会是之前数组的两倍

这是我的第一次尝试

public static Object[] badCopyof(Object[] a, int newLength){ // not useful
        Object[] newArray = new Object[newLength];
        System.arraycopy(a,0,newArray,0,Math.min(a.length,newLength));
        return newArray;
}

这里给出一个数组 , 现在这是一个 Object 数组 , 这里是一个 Employee 数组这没问题, Employee 就是 Object所以一个 Employee 数组, 就是一个 Object 数组 , 这是我们的目标程度 ,现在建立有这个目标长度的一个数组 , 现在复制所有的元素

为此有一个工具方法 ,不过我也同样可以实现, 这个方法然后返回这个新数组 , 实际上这个方法完全没用
因为他返回的是一个 Object 类型的数组, 完全不同 , 如果我尝试把返回的这个数组, 用作一个 Employee 数组这是不行的 , 所以下面利用反射, 用不同的方式来实现

public static Object goodCopy0f(Object a, int newLength) {
    Class cl = a.getClass();
    if (!cl.isArray()) {
        return null;
    }
    Class componentType = cl.getComponentType();
    int length = Array.getLength(a);
    Object newArray = Array.newInstance(componentType, newLength);
    System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength);
    return newArray;
}

这里是我原来的数组, 这是目标长度, 现在我要使用一个方法 , 来确定这到底是不是一个数组, 所以我要得到 Class 对象, Class类的 isArray方法会告诉我这是不是一个数组 , 如果是 还有一个方法名为 getComponentType 这会告诉我这是什么类型的数组, 下面来使用Array

可以让他提供这个东西的长度, 我现在知道这个东西是一个数组, 而且可以让他为我创建这样一个数组的另一个实例, 要是这个类型, 而且是指定的长度; 现在我会得到一个正确类型的数组Array.newInstance就会为我创建一个 Employee 数组, 因为元素类型是 Employee , 然后再使用已有的辅助方法, 把原来的元素复制过来 再返回这个新创建的数组
让人惊奇的是这 goodCopeOf 方法不仅适用于对象数组,甚至也适用于基本类型的数组,

int[] a= { 1,2,3,4, 5 };
a = (int[])goodCopyOf(a,10);

所以如果有一个整数数组我可以调用这个 goodCopyOf让他为我创建这个数组的一个副本, 但是要有10个元素这个方法也能很好的工作

确实我要把它强制转换回一个整数数组, 因为 goodCopyOf 返回时

哼我们来看看他会返回什么
有意思的是他返回的是 Object, 而不是一个数组, 而且他的参数类型也是 Object, 而不是一个数组, 这是因为基本类型的数组, 比如int[]当然是一个 Object, 任何数组都是 Object, 但他不是一个 Object 数组, 所以这是要注意的一个有意思的小问题

在示例程序中, 我加入了前面看到的 badCopyOfgoodCopyOf 方法


这里来具体使用这些方法,你可以看到我要创建一个 int 数组的副本, 这里要创建一个 String 数组的副本, 然后这里使用 badCopyOf 建立一个 String 数组

大家应该还记得 badCopyOf 的问题, 他会返回一个 Object 数组, 我试图把这个数组强制转换为一个 Stringge 数组时, 这个强制转换会失败, 我们会看到一个异常

在这里可以看到整数数组的副本, 注意现在他已经扩展为有更多的值, 那些值都是0, 因为 int的默认值就是0, 这里可以看到 String 数组, 现在他也扩大到有更多的元素, 新加的元素都是 null, 下一个调用不出所料确实抛出了一个异常, 将一个 Object数组强制转换为一个 String数组是不合法的

再次注意这个奇怪的数组记法:中括号L 分号 分号中间是元素类型

这是一个 Object 数组, 这里是一个 String 数组, 这两个类型之间的强制转换是不合法的, 你已经了解了如何读取和设置任意的字段, 而且知道了如何查看任意的数组,

利用反射可以做的另一件事是调用任意的方法, 首先你可能想知道有哪些方法

Method ml = Employee.class.getMethod( "getName" );
Method m2 = Employee.class.getNethod( "raiseSalary" , double.class);

如果有一个 Class对象, 可以让他查找一个特定的方法, 为此要提供这个方法名和他的参数类型, 由于重载可能有多个方法, 或者可以调用 getMethod来得到所有这些方法的一个数组之前已经见过
一旦有了一个 Method的对象

String n = (String) m1.invoke(harry);
double s = (Double) m2.invoke(harry);

可以利用这个对象来访问方法, 就像可以利用 Field 的对象访问字段一样, 然后可以调用这个方法, 具体做法是调用 invoke, 并传入隐式参数和这个方法需要的任何显示参数

稍后我会给出一个应用, 其中将打印一个任意函数的值表格这是一个很好的例子, 因为可以传入任意函数
并打印相应表格, 不过在深入之前要知道, 使用反射只是对非常通用的工具才有用, 比如说调试器或解释器
或者对象关系映射器

诸如此类对于大多数情况, 还有一种更好的解决方案就是使用接口和 Lambda表达式


这个示例程序, 得到两个 Method的类型的对象, 第一个是计算平方的方法, 稍后会给出这个方法, 第二个是平方根方法, 这是从 Math类得到的方法, 我们让 Math类提供这个平方根方法 , 参数为 double 类型, 这里定义了 square 方法, 可以看到他就是计算这个数的平方 , 在这里我说我想要这个类的 square 方法, 就是我现在正在定义的这个类, 然后把它放在这个 Method的引用中 , 然后调用两次 printTable

这里指出利用这个 square 方法打印一个表格, 显示从1到10的结果, 而且要提供10行, 在这里对 sqrt 方法做同样的事情, 所以 printTable 是这个程序的关键

他取起始值结束值函数和这个 Method的对象, 这是一个完全任意的方法他适用于任何方法, 下面调用这个方法, 假设这个方法是一个静态的方法, 因此为隐式参数提供一个null, 让 x 取值为从 from to
要有适当的间距来得到所需的行数, invoke 返回一个结果 , 现在我们把它强制转换为 double, 然后打印结果, 下面来看输出

这里可以看到 square 方法和 sqrt 方法, 表格里有所调用方法的参数和结果, 这里的重点是生成这个表格的方法 , 可以接受完全任意的 Method的对象

这一课中
你已经了解了如何使用 class.newInstance 方法, 但是这个方法在java9中已经被废弃
原因很有意思newInstance方法对于检查型异常表现不太好
在这里我有一个类

class Fred {
	public Fred () throws IOException {
		throw new IOException( "No Fred.properties" );
	}
}

他有一个无参数构造器总是抛出一个异常
在这里我有一个方法

public Fred haveFred( ) {
    try {
        return Fred.class.newInstance();
    } catch (ReflectiveOperationException ex){
        System.out.println("Caught ROE"); 
        return null; 
    } 
}

使用 class.newInstance 构造 Fred的一个实例如果不成功ReflectiveOperationException 异常, 我们就会得到一个消息, 指出我们捕获了这个异常,并返回null
现在问题是, 当然这不是真正抛出的异常, 他会抛出一个 IO 异常, 可能在JShell中具体演示会更清楚一些, 下面复制粘贴这个类, 你会注意到这里统统都加了大括号, 这样JShell就会知道这些都属于同一个定义, 下面复制这个方法

现在当我调用 haveFred 时 , 来想想看这里会发生什么 , 我们要构造这个新实例, 他要抛出一个 IO 异常
, 现在他真的能捕获这个异常吗? 答案是否定的
这是检查型异常, 这里我们力图打败java类型系统, 究其核心, 问题出在 class.newInstance 上, 应该改为调用构造器的 newInstance 方法 , 这就不会有这个问题

Fred.class.getDeclaredConstructor().newInstance();

他会捕获所有检查性异常, 并把它包装在一个 InvocationTargetException 中, 为此只需要把 class.newInstance 调用替换为 class.getDeclaredConstructor().newInstance();. 然后所有关于废弃的警告就会消失

String.class.getConstructors()
	[Ljava.lang.reflect.Constructor;@706a04ae

java9还有另外一个变化, 这与JShell显示数组的方式有关,在视频中, 我使用的JShell版本, 总是通过调用 toString, 显示一个数组, 否则你会得到类似这样奇怪的结果

String.class.getConstructors()
$17 ==> Constructor[15] { 
public java.lang.String (byte[]),
public java.lang.String ( byte[],int ,int) ,
public java.lang.String( byte[],java.nio.charset.CharseRet),...}

java9态度有所好转,他会在每个元素上调用 toString, 来显示具体的数组元素, 所以现在完全不再需要在JShell 中调用 toString 了你肯定会喜欢这个改变

7.0有效的使用继承

上一课的最后我给出了类的一些设计技巧这一课的最后还是一样我会关于如何适当的使用继承给出几点建议

首先设计一个类层次结构时, 总是要把公共的操作和字段, 移到超类中, 这样一来他们就不会重复, 但是不要使用 protected, 也就是受保护的字段 protected 字段弊大于利
因为这些字段允许所有此类访问, 而你永远也不能将他们删除, protected 方法是可以的, 不过不是很常用

要记住继承模型是一个 is-a 关系每个经理 Manager都是员工 Employee , 如果不存在 is-a 关系就不要使用继承

例如如果有 Employee Manager, 现在你还想为钟点工(Contractor) 建模
哼钟点工是员工吗? 钟点工可能按小时计薪而不拿工资
如果让 Contractor扩展 Employee

public class Contractor extends Employee{
	private double hourlyWage; // ???
}

现在 Contractor有一个 hourlyWage 字段, 还有一个从 Employee 继承的 salary 字段
这听上去不太对, 如果听上去不太对, 可能就需要重新组织你的层次结构, 除非你继承的所有方法都是有道理的, 否则不要使用继承 , 下面给出一个例子

class Holiday extends GregorianCalendar { . .. } // ???
...
Holiday christmas;
christmas.add (calendar.DAY_OF_MONTH,12);

第四课中介绍的 GregorianCalendar类, 实现了日历上的一天, 日历日应该还记得吧, 现在我们希望有一个类 Holiday, 每个假日肯定都是日历日, 所以我们想在这里使用继承

但实际上这样不行因为 GregorianCalendar有一个更改器方法 add, 允许把一个日历日期向后移一定的天数, 这里 christmas是一个 Holiday, 应用 add方法, 我要为 christmas增加12
这还是一个假日吗? 可能不是

add方法并不适用于 Holiday, 这里使用了继承但是这不合适, 这种情况下就不能使用继承, 要确保超类的所有方法都确实能工作, 坦率的讲这里的问题在于更改, 如果通过扩展 LocalDate 来实现假日, 则是完全可以的因为 LocalDate 不能任意更改, 人们总是想修正这个问题

不过覆盖一个方法时, 不应该改变期望的行为, 你不能说 哼, 我想在Holiday的子类中, 改变add对于假日的含义, 因为这与多态不一致

int d1 = x.get(calendar.DAY_OF_MONTH);
x.add(calendar.DAY_OF_MONTH,1);
int d2 = x.get(calendar.DAY_OF_MONTH);
system.out.println(d2 - d1);

如果有一个 x这可能是一个普通的日历日或者是一个假日, 现在我们想为他加一天, 我们希望新的一天是后一天, 而不论他是否是一个假日, 我们不希望存在这样一种情况 , 即这个代码的运行, 要依赖于对象的性质,

  • 不要试图在覆盖方法时改变期望的行为
  • 设计原则:对于已经投入使用的类,尽量不要进行修改,推荐定义一个新的类。来重复利用其中的共性内容,并且添加改动新内容。

否则这会把你的用户搞糊涂, 多态确实是你的得力帮手, 要尽可能的充分利用他,

if (x is of type 1){
    action1(x);
}else if (x is of type 2){      ==> x.action();
    action2(x);
}

如果你发现要使用 instance-of关系写代码 , 比如说如果 x 是这个类的一个实例, 做某件事情, 如果 x 是另一个类的实例, 就做另一件事情 , 就要试试看能不能重新组织这个代码, 使要做的这件事情或者另一件事情成为超类的一个方法, 因为在这种情况下 可以直接调用这个方法 而由多态找出合适的实现

最后我已经介绍了很多有关反射的内容, 因为他很有意思, 而且展示了虚拟机所做的一些工作, 并且提供了一些合法的用例, 不过在你开始使用反射之前, 先问问自己, 有没有更好的办法来解决这些问题, 以上就是关于继承要介绍的全部内容

5、接口、Lambda表达式和内部类

接口:就是一种公共的规范标准,只要符合规范标准,就可以通用;(接口就是特殊的抽象类)

接口就是多个类的公共规范

接口是一种引用的数据类型,最重要的就是其中的:抽象方法


这一课我们要学习接口这个概念,究其核心其实非常简单 却有着深远的影响。

  • 接口就是符合这个接口的类,必须提供的特性的一个描述

通过使用接口,你可以清楚的表明一个类的职责,并提供服务来支持所有愿意符合这些要求的类,然后我们再介绍 Lambda表达式。

正是 Lambda表达式,使得使用接口极其简单。这一课的最后我们会详细的分析内部类,历史上他出现在Lambda表达式之前, 比较复杂的情况下内部类仍然是必要的


0.0接口的静态方法

从Java8开始,接口当中允许定义静态方法

格式: 就是将default(默认方法)或者abstract(抽象方法)改为static(静态方法)

public interface MyinterfaceAbstract{  //接口类
    public static 返回值类型 方法名称(参数列表){  //静态方法
    //方法体
    }
}

注意:

错误用法,不能通过接口实现类的对象(new)来调用接口当中的静态方法 //因为默认方法中有相同名称的方法会报错,使用需要重写

正确用法:通过接口名称,直接调用其中的静态方法 格式”接口名.静态方法名”

接口名称.静态方法名(参数);
//参考代码    
MyinterfaceAbstract.top();

Java版本的接口特点

如果是Java7,那么接口的内容包括

1.常量

2.抽象方法


如果是Java8,那么接口的内容额外包括

3.默认方法 默认方法也可以被覆盖重写

public default 返回值类型 方法名称(参数列表){方法体}

4.静态方法

格式:public static 返回值类型 方法名称(参数列表){方法体}

注意:

应该通过接口名称进行调用,不能通过实现类对象调用接口静态方法


如果是Java9,那么接口的内容额外还包括

5.私有方法

普通私有方法 :  private 返回值类型 方法名称(参数列表){方法体}

静态私有方法 :  private static 返回值类型 方法名称(参数列表){方法体}

注意:
private的方法只有接口自己才能调用,不能被实现类或者别人使用


在任何版本的接口中,接口都能定义抽象方法.

格式:

public interface MyinterfaceAbstract{
    //这是一个抽象方法
public abstract void methodAbs();   //public abstract返回值类型 方法名称(参数列表);
}

注意:

1.接口当中的抽象方法,修饰符必须是固定的两个关键字:public abstract

2.这两个关键字修饰符,可以选择性地省略

3.方法的三要素可以依据需求省略

在java9版本中

1.成员变量其实是常量 格式:public static final 数据类型 常量名称 = 数据值;

注意:

常量必须进行赋值,而已一旦赋值将不能改变

常量名称完全大写,用下划线进行分隔.

1.0理解接口概念

在java中接口就是类必须满足的一组要求

当然前提是这些类选择要符合这些接口, 接口本身不是类, 就像我前面说过的接口是一组要求,我们很喜欢接口 。原因是利用接口,我们能写出提供有用服务的提供者,服务提供者会说,如果你的类符合一个特定的接口,我就会完成这个服务。

定义一个接口

public interface 接口名称{
	//接口内容
}

class关键字变为interface;

备注:换了关键字的interface之后,编译生成的字节码文件依然是: Java.class


下面来看一个简单的例子Arrays 类中有一个方法 sort他会对数组排序,前提是这个数组中的元素,属于一个符合 “Comparable 接口的类

下面来简单看一下 Comparable 接口, 这个接口非常简单,就是这样

public interface Comparable{
	int compareTo(Object other); // automatically public
}

接口中的方法默认都是public abstract类型的

可以看到这里用关键字 interface,而不是 class 声明这个接口,这里是接口名, 接口可以有多个方法, 这里的这个方法是一个抽象方法,他没有具体实现

这个方法名为 compareTo稍后我们会分析他到底要做什么, 另外要注意,并没有声明这个方法为 public,接口中的方法会自动作为 public 方法, 任何希望符合这个接口的类,都必须实现这个 compareTo 方法 , 从而能比较对象, 下面来看这一部分

public class Employee implements Comparable{
    public int compareTo(Object otherObject){
        Employee other = (Employee) other0bject;
        return Double.compare(salary,other.salary);
    }
    ...
}

我们已经有一个接口 Comparable, 现在有一个类选择实现这个接口, 为此这个类要加上 implement 关键字, 后面是他想要实现的一个或多个接口, 然后这个类, 在这里就是 Employee类,要实现这个 compareTo 方法

接口的实现类必须覆盖重写(实现)接口中所有的抽象方法

如果实现类并没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类


现在这个方法会比较两个员工, 在上一页已经看到, comparedTo 方法有一个 Object 类型的参数,所以实现这个方法时, 我们必须使用完全相同的参数类型, 因此这里将这个 Object 强制转换为具体的Employee然后想比较他们的工资, 所以我们会说 , 这些员工按他们的工资排序, 这可能有点不合情理

不过这里只是用他作为一个例子, 工资的比较利用这个辅助方法完成,这是 Double 类中的一个静态方法, 允许我们比较两个浮点数, 在这里就是比较这个员工的工资和另一个员工(other)的工资

接口本身只是说我们需要实现 compareTo , 要了解 compareTo 如何工作必须查看文档 , compareTo 应当返回一个指示符 , 指示第一个对象在第二个对象之前还是之后 , 为此要返回一个正整数0, 或者一个负整数

如果另一个对象 otherObject 应该在前就返回一个正整数, 如果当前对象与另一个对象没有区别,他们的顺序不重要就返回0, 否则就返回一个负整数 Double.compare 方法就能完成这个工作, 如果 salary 小于 other.salary他就返回一个负数

在这种情况下, 当前对象应当在前, 如果两个工资相同就返回0, 否则返回1, Double.compare 是一个很好用的方法 , 因为你能很容易的得到比较结果, 他甚至了解一些特殊情况, 如正无穷大和负无穷大 , 不过对于工资不存在这些情况

有一点使这个 Comparable 接口有些不太好用 , 就是 compareTo 方法接受任意的 Object , 实际上有一个泛型版本的 Comparable 接口他有一个类型参数, 可以使他更好用一些

class Employee implements Comparable<Employee>{
    public int compareTo(Employee other){
        return Double.compare(salary,other.salary);
    }
}

在最后这些代码中可以看到, 这里是 Employee 类 不再是实现基本的 Comparable, 现在要实现 Comparable<Employee> , 这里的这个类型现在是 compareTo 方法的参数类型, 所以这里 compareTo 方法, 会有正确的参数类型 Employee然后我们只需要比较工资 ,这里不再需要强制类型转换,
我从基本的 Comparable 方法开始谈起就是那个没有类型参数的 Comparable因为从概念上看他更简单

但实际上使用这种带类型参数的版本, 意味着 comparedTo 方法更易于实现

下面来考虑一下 Arrays.sort要做些什么, 你可能在大学里学过算法课程, 了解有关排序算法的所有知识
或者也可能不了解, 不过这并不重要

if (a[i].compareTo(a[j])>0){
	// rearrange a[i] and a[j]
    ...
}

因为基本来说排序算法要做什么, 他会不断比较元素,如果对顺序不满意, 他就会以某种方式重新排列元素的顺序, 所以如果查看Arrays.sort 的源代码可能会看到类似这样的代码

我有第i个元素 , 他要与第j 个元素比较, 如果他们的顺序不对, 我就想重排他们的顺序 ,所有排序算法基本上都是做同样的事情 , 当然他们在如何进行比较和重排顺序的细节上有所区别, 不过就目前来讲, ,我真正感兴趣的是编译器怎么知道, 有一个 compareTo 方法可以调用, 这里有一个元素 a[i] , 现在我们在这个元素上调用这个方法

另外你知道的Arrays.sort可以对任何类型的数组进行排序, 嗯, 这里当然要用到接口了a 是一个 Comparable 对象的数组, 因此 a[i]Comparable这是我们知道的对于所有 Comparable对象,我们还知道他会有一个 comparedTo 方法 , 这正是 Comparable接口所要做的 ,就是保证我们有一个 comparedTo 方法, 所以 sort 函数 ,只需要把它的参数声明为一个Comparable 数组

public static void sort(Comparable[] a)

不过说实话如果你在java API文档中查看Arrays.sort, 由于历史原因, sort接受的是一个 Object 数组, 然后再强制转换为 Comparable不过这是一个小细节 , 理想情况下sort 应当接受一个 Comparable 数组

package com.horstmann.corejava.pojo;
public class Employee implements `Comparable`<Employee> {

    String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    public String getName() {
        return name;
    }
    public double getSalary() {
        return salary;
    }
    public void raiseSalary(double byPercent){
        double raise = salary * byPercent / 100;
        salary += raise;
    }
    /**
     *Compares employees by salary
     *@param other another Employee object
     *@return a negative value if this employee has a lower salary than
     * otherObject,0 if the salaries are the same,a positive value otherwise
     **/
    @Override
    public int compareTo(Employee other) {
        return Double.compare(salary, other.salary);
    }    
}
-----
public static void main(String[] args) {

    Employee[] staff = new Employee[3];

    staff[0] = new Employee( "Harry Hacker" , 35000);
    staff[1] = new Employee( "carl Cracker", 75000);
    staff[2] = new Employee( "Tony Tester" ,38000 );
    Arrays.sort(staff);
    //print out information about all Employee objects
    for (Employee e : staff) {
        System.out.println( "name=" + e.getName() + " , salary=" + e.getSalary() ) ;
    }
}
output:
name=Harry Hacker, salary=35000.0
name=Tony  Tester, salary=38000.0
name=carl Cracker, salary=75000.0

在这个示例程序中可以看到 Employee 类可以看到他实现了 Comparable<Employee>, 这个类的大多数代码你都已经见过不过这里有一个 compareTo 方法他只比较工资, 这是我的测试程序,我们要建立一个 Employee 数组, 然后把它传递给 Arrays.sort()这实际上就是这个程序要做的核心工作, 然后再按员工的工资 , 打印这些员工信息 , 可以看到之前他们是无序的 , 运行这个程序后. 现在他们是有序的 , 这就是这个程序的全部内容

其含义在于排序是一个服务 , Arrays.sort()会为选择实现 Comparable 接口的任何类, 提供这个服务Employee 就实现了这个接口 , 所以他可以利用这个服务

2.0理解java接口的属性

你已经了解了基本接口概念的实际运用 ,下面来更详细的分析接口的性质, 以及如何使用接口
首先接口不是类 ,所以不会有接口对象

你不能实例化接口

x = new Comparable(...); // Error

例如如果你想通过 new Comparable创建一个 Comparable 对象,就会得到一个编译器错误不能对接口应用 new ,

Comparable x; // ok

不过可以有一个接口类型的变量 ,如果一个变量 xComparable 类型 这是完全可以的

x = nwe Employee(...); // ok provided Employee implements `Comparable`

但是如果把一个对象放在这个变量中, 或者更确切的讲放入一个对象引用, 这必须是一个具体对象的对象引用, 这个对象必须属于某个实现了这个接口的类, 所以在这里 x 类型为 Comparable , 所以完全可以在这里放入新建的一个Employee 的引用 , 因为 Employee 实现了 “Comparable 接口 , 可以在 instanceof 检查中使用接口

if (an0bject instanceof Comparable) {...}

这里有一个变量 anObject 我们想检查他是否是 Comparable就可以使用 instanceof 并提供接口 , 这与类的检查是类似的如果这个检查通过了, 还可以强制转换一个接口, 可以有接口的层次结构, 这不太常见不过确实是可以的

public interface Moveable{
	void move(double x, double y);
}
public interface Powered extends Moveable{
	double milesPerGallon( );
}

这里有一个假想的接口 Movable , 还有一个扩展了 Movable 的接口 Powered , 这说明任何人想要实现 Powered , 都需要实现这个接口中的方法 , 以及其超接口中的方法

一个接口可以有字段, 但是这些字段会自动作为 public static final , 字段这说明他们自动为常量
这里给出一个例子

public interface Moveable{
    . . .
    double SPEED_LIMIT = 95; // automatically public static final
}

这个例子同样有些随意 , 这里的 SPEED_LIMIT 字段是一个常量 , 可以用 Movable.SPEED_LIMIT
引用这个字段, 接口里不能有常规的实例变量, 要记住接口不是类. 并没有任何对象 , 所以也不会有实例字段 , 既然不会没有任何实例, 也就不应该有实例变量, 否则是没有道理的

public class Employee implements `Comparable`,Moveable { ... }

一个类只能扩展一个类 ,你只能有一个超类, 但是一个类可以实现任意多个接口, 可以有一个实现了两个接口的 Employee类 ,同样这种情况也不常发生, 不过确实有可能发生, 这是完全合法的, 个类可以在扩展一个类的同时, 实现一个接口 , 或者实现多个接口, 不过一个类最多只能扩展一个超类, 现在你可能想知道为什么需要接口 , 使用继承有什么问题呢?

abstract class Comparable {	// why not?
	public abstract int compareTo(0bject other);
}

难道不能把 Comparable实现为一个抽象类吗?
Comparable 接口中的 comparedTo 方法是未定义的, 他是抽象的 , 不过我们并不是因为这一点而需要接口, 也可以把 compareTo 声明为一个抽象方法 ,

class Employee extends Comparable {	// why not?
	public int compareTo(Object other) { . . . }
}

在这种情况下Employee 类可以选择扩展 Comparable而不是实现一个接口 , 在这里的排序试验中Comparable 是一个类Employee 可以扩展这个类 , 没错这也能很好的工作, 不过问题是 , 如果 Employee 还想扩展另外一个类呢?

class Employee extends `Person`,Comparable // Error

假设 Employee 想扩展 Person , 在这种情况下Comparable 就不能作为一个类 , 因为在java中一个类只能有一个超类 , 这种情况下接口确实很方便 , 因为除了扩展一个类之外 , 你可以增加你想要的任意多个接口 . 所以java没有所谓的多重继承, 不能同时继承多个类 , 而接口提供了一种受限的多重继承, 既有多重继承的大部分好处 , 又没有相应的技术复杂性

有一些编程语言提供了多重继承, 但是很难组织来自多个超类的数据, 接口不能携带任何数据 , 他们能做的只是告诉你有哪些功能 , 在这种情况下汇集多个超类的功能是没有问题的, 问题出在数据上 , 除了在接口中见过的抽象方法外 , 还可以在接口中增加静态方法, 原先这是不允许的 , 没有技术原因禁止这么做 , 只是感觉好像有些违背接口提供抽象规范的初衷 , 所以以往看到的接口中不包含任何代码, 不过现在你知道的, 有人说为什么接口中不能有静态方法呢?在java API中你会看到有些接口有”伙伴”, 就是提供工具方法的伴随类, 如 Collection和 “Collections, Path Paths 等等

这个伴随类带有一个 s, 这会给你一个提 , 说明它包含一些静态工具方法 . 如今不再需要这么做了, 你可以把静态方法直接放在接口里 , 下面给出这样一个例子

public interface Path{
    public static Pathget(String first,String. . . more){      
  	  return FileSystems.getDefault().getPath(first, more);
    }
    ...
}

你已经见过 Path.get 方法可以把一个字符串转换为一个文件路径, 在Java API中, 这个方法要放在一个单独的类中, 如今可以只有一个接口 Path , 在这个接口中我们可以放入一个静态方法 , 这个静态方法 , 与所有其他静态方法的工作是一样的 ,他不妨问任何实例变量 , 当然接口也没有实例变量 , 他只是以某种方式处理输入即参数

所以现在如果你有一个接口 , 而且觉得有一些有用的伴随方法 , 可以把他们作为静态方法直接放在接口中


接口的成员变量

接口当中也可以定义”成员变量”,但是必须使用 public static final三个关键词进行修饰. 从效果上看这就是接口的常量

  1. 接口当中的public static final关键字可以省略不写

  2. 接口当中的常量,必须进行赋值,不能不赋值

  3. 由于使用final修饰,所以不可以在类中给他赋值

  4. 接口中常量的名称,使用完全的大写.用下划线进行分隔

  5. 由于使用public static 修饰,接口里常量是可以直接在类里面被调用的

格式:

public static final 数据类型 常量名称 = 数据值
//格式   注意!一旦使用了finale关键字进行修饰,说明不可改变
public interface pz{  //接口
	public static finale int NUM_ONE = 10; //其实可以不写这三个关键字,会自动默认为写了
}

使用接口的常量

public static void  main(String[] age){
       //接口名.常量名  打印输出10
       Sytem.out,println(pz.NUM_ONE);
}

3.0使用接口的默认方法

默认方法的定义格式:

public interface 接口名称{  //接口类
    public default 返回值类型 方法名称(参数列表){  //默认方法
    //方法体
    }
}

接口的升级: 接口需要添加新的方法

//调用默认方法,如果实现类当中没有会向上找接口

  1. 接口的默认方法,可以通过接口实现类对象,直接调用

  2. 接口的默认方法,也可以被接口的实现类进行覆盖重写

  3. 接口的方法的默认修饰符为public abstract, 所以实现接口的方法必须用修饰符public


从Java8开始,接口里允许定义默认方法

刚开始介绍接口时我给出了一个包含一个抽象方法的接口, 现在你知道了还可以在接口中加入静态方法, 接口中可以包含的第三种方法是默认方法, 一个接口可以包含他的任何方法的一个默认实现 ,而不是将方法声明为 abstract

public interface Comparable<T>{
	default int compareTo(T other) {
    	return 0; 
    }// By default, all elements are the same
}

例: 我们可以把 Comparable 接口定义如下 , 可以让 comparedTo 作为一个默认方法, 使他返回0 , 这样一来如果有人实现了 Comparable接口, 倘若他们对这个默认值还算满意, 就不用另外实现 compareTo , 这个默认实现意味着所有元素都是一样的, 这可能不太让人信服 , 所以下面来看一个更让人信服的例子 ,

public interface MouseListener{
	default void mouseclicked (MouseEvent event){}
    default void mousePressed (MouseEvent event){}
    ...
}

JavaAPI 中有一个名为 MouseListener 的接口 , 其中收集了一组方法. 可以对鼠标点击, 鼠标按键, 按钮松开, 按钮按下等做出反应

在大多数情况下 , 实现这个接口的人 , 只想加入这里的一个或者可能两个事件 ,但是目前他们必须加入
所有这五个事件处理方法 ,如果原先 MouseListener 的实现者 , 把 MouseListener 接口的所有这五个方法 , 都实现为无参数的默认方法就好了 , 这样一来别人就可以 , 有选择的覆盖他们真正感兴趣的那些方法 , 为什么没有这么做呢?

这只是由于历史原因MouseListener 接口很古老, 而默认方法是java8才增加的 , 需要说明的是, 默认方法可以调用抽象方法
这里给出一个例子

public interface Collection{
    int size(); // An abstract method
    default boolean isEmpty() { return size() == 0; }
    ...
}

这里有一个 Collection接口 , Collection有一个 size() 方法 , 不过当然了可以用各种各样不同的方式实现 Collection , 分别采用自己的方法计算 Collection的大小所以这是一个抽象方法

在接口这一集 , 实现这个方法是没有意义的 ,不过这里给出第二个方法 isEmpty 方法, 对于这个方法
我可以提供一个具体实现 , 我可以说如果一个 Collection的大小为0 , 这个集合就为空 , 需要把这个方法声明为一个默认方法 , 因为任何非抽象方法都需要声明为 , static default

注意这里的这个方法 , 并没有访问任何实例变量 , 毕竟接口没有实例变量 , 不过他确实调用了另一个方法 , 他调用了一个抽象方法 . 我们甚至不知道这个 size方法是如何实现的, 但是实现了这个接口的任何具体类, 都必然会实现这个方法 , 所以这里可以有一个 size()方法 , 能调用这个方法查看大小是否为0

以前还没有出现默认方法时, 设计接口的人不愿意在接口中放入太多的方法 , 因为对于实现者来说这会很繁琐 , 他们必须提供所有这些方法的实现 ,如今有了默认方法 , 你可以加入一些抽象方法 , 再利用这些抽象方法合成另外一些便利方法 , 这是一种相当常见的机制

关于默认方法 , 还有另外一个很有意思的想法 , 考虑 Collection接口 , 这个接口在java中已经存在很多年了, 当然人们会以多种方式实现这个接口 ,假设有人提供了一个类, 比如一个名为 Bag的类 , 他以某种方式实现了 Collection ,现在我们想演化这个 Collection接口

有人说我们有些新想法 , 我们想为接口增加一个方法 ,这种情况确实已经发生 ,在 jdk 8中Collection ,接口中增加了一个名为 Stream 的方法 , 这个方法非常有用, 那么我们的 Bag会怎么样呢, 如果 Stream不是默认方法 , Bag类就无法编译 , 因为如果你试图重新编译这个类 , 编辑器会查看 Collection, 他会说 奧 , 现在 Collection有这个 Stream方法 , 但是你的类里没有这个方法 ,编译失败

如果有人偷偷用java的一个老版本 , 编译 Bag,并把这个类文件放在一个JAR文件中, 然后链接到你的程序
会发生什么呢 , 这确实是可以的 , 程序会继续运行, 当然前提是没有人调用这个新方法 , 虚拟机会负责查看这个新方法 , 是否确实存在 , 因为虚拟机知道文件可能来自不同的版本 ,如果有人调用了这个方法 ,虚拟机别无选择 , 只能抛出一个异常 , 但是如果没有人调用这个方法 ,这个改变仍能顺利通过

所以我们说 , 接口演化会带来这样一种情况 ,即不能保证源代码兼容, 但是能保证二进制兼容 ,不管怎样
演化接口都是一件危险的事情 ,以前人们总是避免这样做 , 如今这变得容易了, ,因为你可以简单的标记接口方法 ,把它增加为一个默认方法 , 这样两个问题都能解决

例如在java8中增加 Stream方法时 , 如果现在实现 Bag的程序员 , 重新编译他们的代码, 就会自动选择这个默认方法 , 如果他们没有编译代码 , 运行的程序仍然能调用这个方法 ,所以默认方法非常棒 , 利用默认方法可以在一个接口中 ,放入原先不能加入的更多功能 , 不过默认方法确实也存在一个问题 , 如果从两个不同的路径 , 选择相同的方法会有问题 ,假设你实现了两个接口 ,或者扩展了一个类另外实现了一个接口 ,他们都有一个具体方法 ,包含具体代码而且同名 , 这就需要有人在这两个方法中 , 选择使用哪一个
这种二异性 , 正是我们最初不希望java提供多重继承的原因之一

不过现在我们就处于这样一种有多种选择的情况 , 即代码可能通过超类或者超接口提供 , java的设计者
决定为我们提供两个简单规则来处理这种情况 , 这些规则很容易学也很好理解 , 这些规则名字很直接了当
分别是接口冲突超类优先

如果从一个接口得到一个默认方法 , 而另一个接口提供了一个同名方法 , 不论他是默认方法还是抽象方法 , 作为程序员你必须解决这个冲突, ,下一页我会给出一个例子 , 所以只要有可能出现冲突, ,程序员就必须解决冲突

另一方面如果你从超类选择了一个方法 , ,那么超类总是优先于所有其他接口中提供的任何默认方法 , 超类优先这个规则听上去可能有些随意 , 但这是为了保证兼容性 , 下面通过两个例子来说明这些规则, 首先是接口冲突规则

interface Person { 
    default String getName( ) { return "John Q. Public"; }; 
}
interface Named {
    default String getName() { 
        return getClass().getName( ) + "_" + hashCode(); 
    } 
}

我有一个接口 Person, 他有一个默认方法会返回 John Q. Public , 另外还有一个接口名为 Named , 他也有一个 getName() 方法 , 这是一个默认方法, 这是他生成的名字他会得到类名 ,另外出于某种原因
再加上散列码

现在如果一个类同时实现了 PersonNamed会发生什么

class Student implements Person,Named{
    public String getName( ) { return Person. super.getName( ); }
    . . .
}

在这种情况下 , 编辑器拒绝在这两个方法中做出选择 , 实际上他不会编辑你的类, 编译器会指出你必须在实现 PersonNamed的这个类中提供一个 getName 方法 ,所以我们这样做了 , Student实现了 PersonNamed而且他实现了 getName() 方法

当然现在他能生成自己的实现与其他实现无关, ,或者也可以在已经提供的两个方法中选择其一 , 并显示调用那个方法 , 这里有一些语法要求 , 如果 Student要实现 getName() , 认为 Person.getName() 就很好 , 则要调用 Person.super.getName()可以看到这个语法 , 首先是你想要的接口名 ,然后是.super
再加点和这个方法名

不管怎样语法就是这样的 , 有意思的是 , 即使 Named.getName() 是抽象的 , Student也必须实现 getName() , 规则是如果有一个默认方法 , 或者可能有两个要选择的默认方法 , 实现类必须做出决定 , 不过如果两个方法都是抽象方法, 即如果 Person 中的 getName 是抽象的 , Named 中的 getName 也是抽象的 , 这种情况就于默认方法出现之前我们经常遇到的情况是一样的

没问题 student 有两个选择 , 他可以实现 getName , 或者可以把 Student声明为 abstract , 这样 getName 就是一个抽象方法 , 上一页看到的规则并不经常用到, 因为一个类实现两个接口的情况不常发生

不过确实 , 经常会有一个类不仅扩展一个类同时还实现一个接口 ,

class Student extends Person implements Named {...}

在这种情况下规则相当简单 , 如果一个接口有一个默认方法 , 而超类也提供了同样的方法, 那么超类优先, 这同样是为了保证兼容性和接口演化 , 可以在一个接口中根据需要增加多个默认方法 , 这对现有的代码没有任何影响 , 已经看到在java8之后 , 接口可以有静态方法和默认方法
java9还支持 private private static 方法

所以从现在开始 , 接口中的任何方法要么是抽象方法 , 需要在一个实现类中被覆盖 , 要么就是 default staticprivateprivate static 方法

为什么一个接口中想要有私有方法呢? 能调用这些私有方法的只能是默认方法或静态方法 , 或者可能是其他私有方法 , 所以这些方法唯一可能的用例 , 就是用于抽取出这个接口中找到的公共代码 , 这些方法不能由实现这个接口的任何类调用

4.0熟悉接口调用

你已经对使用接口的机制有了很多了解下面来看一些常见的用例 ,

最常见的用例就是回调 , 回调是出现某个事件时要发生的动作 ,

例如经过一个时间间隔时 , 计时器会执行一个回调 , 用户按下按钮时 , 这个按钮会执行一个回调 , 如此等等这里的基本思想是回调动作在将来的某个时间点发生 , 所以你提供的动作不会现在立即执行 , 否则你完全可以直接把代码放在程序里 , 你提供的是一个以后才发生的动作

举个例子来看java API中的 Timer

public interface ActionListener{
	void actionPerformed(ActionEvent event);
}

可以建立一个计时器 , 表示只要经过一个时间间隔就要做某件事情 , 他要做什么呢 , 哼 , 为此你必须为这个计时器提供另外一些信息 , 去提供一个实现了 ActionListener 接口的对象 , 这里可以看到 ActionListener 接口 , 他有一个方法名为 actionPerformed , 所以计时器会不断调用 actionPerformed , 他向 actionPerformed 提供一个 ActionEvent , 这个 ActionEvent 包含有关这个事件的一些信息 , 事件什么时候发生, 他来自哪里 , 诸如此类

下面建了一个实现这个接口的类,

class TimePrinter implements ActionListener{
    public void actionPerformed(ActionEvent event){
   		System.out.println( "At the tone,the time is " + new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

这里有一个类 TimePrinter, 他实现了 ActionListener, 然后他提供了 actionPerform 的方法的一个实现 , 这个特定方法会忽略这个 ActionEvent , 他只是生成一个新的 Date对象 , 并向控制台打印一个消息显示: At the tone,thetime , 然后是我们从这个 Date得到的时间

然后 哔 的响一声 , java中要响一声可以这样做 , 首先得到一个默认工具包 , 不管他是什么, 反正其中有一个 beep() 方法 , 这个方法会哔的响一声
这就是我们希望在计时器中发生的动作现在需要建立我们的计时器

package com.horstmann.test;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

public class TimerTest {

    public static void main(String[] args) {

        ActionListener listener = new TimePrinter();
        // construct a Timerthat calls the listener    构造一个调用监听器的计时器
        // once every 10 seconds        每10秒一次
        Timert = new Timer( 10000,listener) ;
        t.start();
        JOptionPane.showMessageDialog(null, "Quit program? " );
        System.exit(0);
    }
     static class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent event) {
            Date date = new Date();
            System.out.println("At the tone,the time is " + new Date());
            Toolkit.getDefaultToolkit().beep();
        }
    }
}
output:
At the tone,the time is Sun Apr 10 12:51:35 CST 2022
At the tone,the time is Sun Apr 10 12:51:36 CST 2022
At the tone,the time is Sun Apr 10 12:51:37 CST 2022
At the tone,the time is Sun Apr 10 12:51:38 CST 2022
At the tone,the time is Sun Apr 10 12:51:39 CST 2022
At the tone,the time is Sun Apr 10 12:51:40 CST 2022
At the tone,the time is Sun Apr 10 12:51:41 CST 2022
At the tone,the time is Sun Apr 10 12:51:42 CST 2022
At the tone,the time is Sun Apr 10 12:51:43 CST 2022

如这里所示 , 我们说每10秒做一次, 这里的时间单位是毫秒 , 这是监听器每10秒就要调用一次这个监听器
这里是监听器, 他是一个 ActionListener, 这是接口这个对象构造为一个新的 TimePrinter 对象
TimePrinter 是我们在这里定义的类 , 所以每隔10秒, 就会调用这个监听器的 actionPerform 的方法
执行这里的代码 , 最后必须启动这个计时器 . 使得所有这些能真正发生 , 这里是刚才见过的代码 , 这是计时器我们要启动计时器 , 这里是 TimePrinter 类 , 他实现了 ActionListener 接口 , 运行这个程序时 , 如果耐心等待10秒会出现一个消息 , 相信我确实会逼得响一声 , 这会永远持续下去 , 所以我显示了一个对话框 , 以便退出程序

接口的另一个典型用法是 , Comparator 接口的用法 , 大家应该记得 Arrays.sort可以对一个 Comparable对象数组排序, 这很好 , 不过如果你想用一种不同的方式 , 对这些对象排序 , 该怎么办呢 , 假设我们想按名字对员工排序而不是按工资排序 , 另外如果这些对象 , 属于一个没有实现 Comparable的类又该怎么办呢?

比如说一个库类,

public interface Comparator<T>{
int compare(T first, T second ) ;
}

我们不能修改库类我们能做的是, 可以使用一个不同版本的 Arrays.sort, 这个版本使用一个Comparator , 允许采用任意的方式比较, 这里给出了 Comparator 接口 , 这是一个泛型接口他有一个类型参数与 Comparable接口一样 , 这个类型参数提供了 compare 方法的参数类型 . 这个方法名为 compare而不是 compareTo , 因为他有两个参数 , 这是我们想要比较的两个对象

除此之外他的工作原理是一样的 , 他会返回一个整数, 可以是一个负数 , 一个0或者一个正数, 这取决于这两个元素相互比较的结果 , 下面我会给出一个实现了Comparator 接口的类的例子

class LengthComparator implements comparator<String>{
    public int compare(String first,String second){
    	return first.length() - second.length();
    }
}

这里有一个 LengthComparator, 会按字符串的长度比较两个字符串, 所以我们实现了 Comparator<String>, 因为 String 是这里 compare 中的参数类型
现在如果第一个字符串 first比第二个字符串 second 的短 , first.length 减去 second.length 的差就是一个负数

所以相比之下, 第一个字符串在前 , 如果他们的长度完全相同 , 这两个 length 之差就是0 , 他们可以按任意顺序出现, 这里排序没有影响, 否则他们的差就是一个正数

String[] friends = { "Peter","Paul","Mary"}
Arrays.sort(friends,new LengthComparator());

具体用法是建了一个 String 数组然后调用 Arrays.sort()并提供这个 String 数组, 以及构造的一个对象, 当然现在这个对象没有任何状态 , 他没有实例变量, 他只有一个 compare 方法 , 在需要确定是否重新排列两个数组元素时. 这个版本的 Arrays.sort , 就会使用这个 Comparator . 其效果是将按长度对这个数组排序所以 Paul Mary 在前 , 然后是 peter

java 有一个名为 Colonobo 的接口 , 可以用来建立真正的对象副本, 这有点复杂所以我们从头开始介绍
大家应该记得, 建立一个对象变量的副本时会发生什么

Employee original = new Employee( "John Public",50000);
Employee copy = original;
copy.raiseSalary(10); 	// oops--also changed original

你原来有一个 Employee 对象 original ,要建立一个副本 copy ,现在你调用一个方法更改 copy 上的对象, 由于 original copy 都指向同一个对象 , 这个更改对 copy 以及 original 都可见 , 现在如果你不希望这样 , 就必须建立一个真正的副本 , 或者按我们的说法就是建立对象的一个克隆, 所以你要对 original 调用 clone 方法

Employee copy = original.clone();
copy.raiseSalary(10); // 0K--original unchanged

clone 方法, 会生成这个对象的一个真正的新副本copy , 然后在这个克隆的 copy 上, 调用一个修改这个副本的方法 , riginal 不会有任何影响 , colognable接口的作用就是帮助完成这个过程, 一个类通过实现 colonobo 接口 , 表明他希望他的实例可以克隆

下面来看这到底是怎么做的

public interface Cloneable {}

奇怪的是colonobo 是一个没有任何方法的接口, 就像你现在看到的 , 他就是这样声明的这有点怪异
, 实际上 clone 方法是 Object 类的一个方法 , 这是一个 protected 方法 , 因为要正确实现这个方法有些难度 , 稍后你就会看到 , 那么 Object.clone会做什么呢?

他会建立一个对象的副本 , 建立一个全新的相同类型的对象 , 然后把所有实例字段复制过来 , 所以如果建立一个 Employee 的克隆, 就要复制name字段复制 celery 字段还要复制 hierDayr字段 , 这里复制字段的意思是指如果这些字段是数字, 比如这里的 salary , 就会得到这个数字的一个副本 , 如果字段是对象引用 , 比如这里的 name 是一个 String 对象引用 , 那么副本就是同一个 String另一个引用 , 这没问题
String 是不可变的 , 所以不论是复制 String 的具体内容 , 还是得到这个 String 的另一个引用 ,实际上并没有什么区别
那么 hierDay 呢 , 如果 hierDayr是一个 LocalDate , 比如我们的例子中 , 他就是一个 LocalDate, 那么同样没有问题 , 因为 LocalDate是不可变的 , 不过假设我们选择了一个不同的实现, 其中 hierDayr 是一个 Date对象 , Date对象是可变的 , 现在 original 和克隆的 copy 共享一个可变的对象 , 如果其中任何一个更改了这个对象 , 另一个也能看到这个更改 , 这其实并不是一个真正的副本, 这种副本称为浅副本或者浅拷贝, 如果所有实例变量都是基本类型 , 或者是不可变对象的引用, 那么浅副本也没问题

不过如果不是这样就需要另外采取纠正措施 , 我说过 clone 方法有些难度就是这个意思 , 实现 clone 方法的人确实需要深入的了解这些细节 , 在类似这样的情况下 ,

class Employee implements cloneable{
    . . .
    public Employee clone() throws CloneNotSupportedException{
        Employee cloned = (Employee) super.clone();
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    } 
}

如果你希望能够克隆对象 , 就需要覆盖 clone , 而且要采用正确的做法下面介绍要怎么做, 首先这里给出 clone 的实现, 稍后我们会分析这个实现 ,其次必须标记你的类 , 指出这个类要实现 Cloneable , 为什么要这样呢 , Cloneable 没有任何方法 , clone 方法实际上来自 Object 超类 , 不过等一下 , 接下来注意我们把 clone 重新声明为 public

他在超类中是一个 protected 的方法 , 这意味着除了此类本身任何其他人都不能调用这个方法 , 通过把它重新声明为 public , 现在所有人都可以开始克隆 Employee , 首先让 Object.clone 提供克隆的新 Employee , 也就是 cloned ,并且复制所有字段, 在这里调用了 super.clone这会调用 Object.clone 方法 , Object.clone 方法检查他是否可以克隆 , 换句话说就是检查所克隆的对象 , 是否属于一个实现了 Cloneable 接口的类

如果不是他就会抛出一个异常 , 指出显然实现者并没有充分理解所有细节我们要退出 , 所以实现 Cloneable实际上就是在说 , 我作为这个类的实现者已经充分理解了这个内容, 我已经了解规则而且实现了这个接口 , 有时这称为一个标记或记号接口 ,这不算是接口的一个很常见的用法

不过java标准库中确实有一些标记接口 , 下面继续 , 克隆了 Employee 之后还要修正这种情况 , 也就是可变字段引用存在的问题 , 我们还要克隆 hierDay把它插入到cloned.hierDay 实例字段中并返回 cloned, 还要注意, 我把 cloned 的返回类型从 Object改为了 Employee, 这是一个有协变的返回类型
, 回忆一下可以让一个覆盖方法的返回类型更特定 , 因为我知道这个返回值是一个Employee

最后这里必须增加CloneNotSupportedException , 这是因为 super.clone , 也就是 Object.clone 威胁 , 有可能抛出一个 CloneNotSupportedException ,在这种情况下确实支持克隆 , 我也可以捕获这个异常 ,这样就不用加 CloneNotSupportedException , 第7课会了解这要怎么做 , 不过我还是选择保持这个签名, 因为有可能 Employee 的一个词类 , 又想抛出这个异常 , 如果有一个 final clone 方法, 就必须使用第7课要学习的技术捕获这个异常 , 所有这些听上去实在是很复杂, 让人眼花缭乱 , 有一点很重要
只有当你确实希望能够克隆对象时才应该这么做 , 不过幸运的是 , 这种情况相当少见java API中只有不到5%的类是可克隆的

如果有一个可变的类 , 你可能想使用克隆 , 如果一个类是不可变的 , 那么还是共享对象引用为好 , 所以如果你有一个可变的类. 仍然可以有一个全新的互不干扰的副本 , 这没有你想的那样常见

例如如果你有一个 PrintStream 或一个 Scanner你希望有他的一个克隆吗? 可能不会这些类没有实现 Colonible , 下面来看示例代码

package com.horstmann.corejava.pojo;

import java.util.Date;
import java.util.GregorianCalendar;

public class Employee implements Cloneable {

    private String name;
    private double salary;
    private Date hireDay;

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

    @Override
    public Employee clone() throws CloneNotSupportedException{
        Employee cloned = (Employee) super.clone();
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }

    /**
     * Set the hire day to a given date.
     * @param year the year of the hire day
     * @param month the month of the hire day
     * @param day  the day of the hire day
     */
    public void setHireDay(int year, int month, int day) {
        Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
        //Example of instance field mutation
        hireDay.setTime(newHireDay.getTime());
    }
    public String getName() {
        return name;
    }
    public double getSalary() {
        return salary;
    }
    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
    @Override
    public String toString() {
        return "Employee{" +  "name='" + name + '\'' + ", salary=" + salary +
                ", hireDay=" + hireDay + '}';
    }
}

这里有一个可克隆的 Employee类 , 这个版本的 Employee类有一个类型为 DatehireDay 字段, 我提供了一个更改器方法, 可以修改这个hireDay, 这里的细节并不重要, 实际上 Date类的 API不是太好, 不过相信我这个方法, 确实会设置为另一个不同的日期, 现在再来看他在测试程序中的工作,

public static void main(String[] args) {
    try {
        Employee original = new Employee( "John Q. Public",50000) ;
        original.setHireDay (2000,1,1);
        Employee copy = original.clone();
        copy.raiseSalary ( 10);
        copy.setHireDay (2002,12,31);
        System.out.println( "original=" + original) ;
        System.out.println( "copy=" + copy ) ;

    } catch (Exception e) {
        e.printStackTrace();
    }
}
output:
original=Employee{name='John Q. Public', salary=50000.0, hireDay=Sat Jan 01 00:00:00 CST 2000}
copy=Employee{name='John Q. Public', salary=55000.0, hireDay=Tue Dec 31 00:00:00 CST 2002}

这里我要创建一个 Employee 类型的对象 , 要把他的 hireDay 设置为某个日期, 然后建立他的一个克隆 , 现在我要更改这个克隆修改他的工资和雇佣日期 , 然后打印元对象 original 和克隆对象 copy , 可以看到 original copy 有相同的名字, 但是克隆对象已经更改 , 他的工资和雇用日期已经修改而原对象不受任何影响, 所以这里正确的完成了克隆


接口之间的多继承

  1. 类和类之间是单继承的(直接父类只有一个)

  2. 类与接口之间是多实现的(一个类可以实现多个接口)

  3. 接口与接口之间是多继承的, 对于接口interface而言,则可以继承多个接口

interface A extends interfaceB, interfaceC(){  //                接口A继承了接口C,接口D
    方法体
}
例如:
public interface One extends Scanner,Runnable {
    void show();
}

注意事项:

  1. 多个父接口当中的抽象方法如果重复,没关系 (个人理解:抽象方法没有具体的内容,所以重复是可以的)
  2. 多个父接口当中的默认方法如果重复,那么子接口必须进行默认方法的覆盖重写[而且带default关键字(因为是子接口)]
  3. 接口没有静态代码块或者构造方法的 (抽象类可以写构造方法)

接口的实现

格式: public class 方法名 implements 接口名一,接口名二{方法体}

public class My implements MyTnterfaceA,MyTnterfaceB{
//覆盖重写所有方法
}
  1. 如果实现类所实现的多个接口存在相同的方法名,只需要覆盖重写一次即可.

  2. 如果实现类没有覆盖重写所有接口当中的所有抽象方法,那么实现类就必须是一个抽象类.

  3. 如果实现类所实现的多个接口当中,存在重复的默认方法,那么实现类一定要对冲突的默认方法进行重写

  4. 如果一个类直接父类当中的方法,和接口当中的默认方法产生了冲突,优先用父类当中的方法. (继承的优先级大于接口)


接口的私有方法 private

问题:我们需要抽取一个共有方法,来解决两个默认方法之间的代码重复问题,但是这个共有方法不应该让实现类使用,应该是私有化的

解决方法:

从Java9开始,接口当中允许定义私有方法

1.普通私有方法解决多个默认方法之间的重复代码问题

格式

private 返回值类型 方法名称(参数列表){
	方法体
}

2.普通私有方法,解决多个静态方法之间的重复代码问题

格式

private static 返回值类型 方法名称(参数列表){
	方法体
}

5.0理解Lanbda表达式如何工作

下面介绍一个很令人兴奋的概念这是java8中新增的一个特性, 就是 Lambda 表达式 , 利用 Lambda 表达式 , 你可以采用一种全新的或者函数式编程方式来写程序 , Lambda表达式允许你采用一种 , 非常简洁的方式 , 编写可以传递的代码块 , 使得块中的代码可以以后执行 , 而且这可以减少原先对于回调 , 必须使用的繁琐的样板代码
比如 ActionListener Comparator
例如

irst.length() - second.length()

按长度比较字符串时, 必须重复调用一个方法 , 通过计算字符串长度之差来比较两个字符串, 以前我们必须把这个代码放在一个方法里 , 这个方法要放在一个类里 , 而且还要让这个类实现一个接口 ,有了 Lambda表达式这就简单多了

Lambda expression (Lambda表达式):
(String first,String second) -> first.length() - second.length()
    
Much simpler than (比以下简单得多):
class LengthComparator implements Comparator<String>{
    public int compare(String first,string second){
   	 return first.length() - second.length();
    }
}

只需要在这里写出这个代码, 在这个代码前面写一个箭头符号就是一个负号-和一个大于号>, 在这个箭头前面把参数放在一对小括号里 , 所以这个 Lambda表达式由两个参数 firstsecond 他们都是 String 类型 ,然后由参数计算这两个字符串的长度之差

为什么把它叫做 Lambda 表达式呢?, 这有一些历史原因20世纪30年代 , 一个研究计算理论的数学家用字母 Lambda, 表示类似这样的函数 , 而不是我们现在所用的箭头 , Lambda在语法中已经消失很久了, 可能更应该把它叫做箭头表达式, 不过Lambda表达式这个名字就这样沿用下来了. 所以Lambda表达式就是提供了要执行的代码, 以及相应的参数列表, 你已经见过了最简单的形式, Lambda表达式可以有一个参数列表 ,箭头 ,然后是一个表达式

还有一些变化形式, 首先如果代码比较复杂无法放在一个表达式里, 就可以使用一个块 ,像平常一样这个块带大括号{}, 还有一个 return 语句
例如在这里

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

我有一个比较复杂的 Lambda 表达式 参数 箭头, 现在这里有一个块包围在一对大括号里, 而且在计算的每个分支里, 都有一个 return ,就像平常的方法一样, 相反如果计算非常简单, 完全可以放在一个表达式里, 那么可以直接写这个表达式, 而不用加关键字的 return

()->Toolkit.getDefaultToolkit( ).beep( );

有时候有些 Lambda 表达式没有参数 , 但是还是要加一个开始和结束括号 , 后面是箭头 , 这样就能清楚的表明 , 这不只是一个表达式而且是一个 Lambda 表达式,

很多情况下使用 Lambda 表达式实, 编辑器都能推倒出参数的类型

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

例如在这里我建立了一个 Lambda 表达式, ,它会比较两个字符串长度之差, 然后我把它存储在这里的一个变量comb 中 , 这个变量的类型为 Comparator<String> , Comparator<String> 总是比较 String , 所以不再需要first,和second的参数类型, 编译器能自己确定, 最后如果一个 Lambda表达式只有一个参数, 而且这个参数的类型可以推倒出来, 那么可以去掉小括号

ActionListener listener = event -> Toolkit.getDefaultToolkit().beep();

这里给出一个例子, 这里有一个 ActionListener, 我把它初始化为一个 Lambda 表达式, ActionListener, 有一个方法 actionPerformed, 这个方法的参数类型是 ActionEvent, 所以可以推导出这个类型, 现在 event 就是一个 ActionEvent

我们没有必要特别指出这一点, 另外也没有必要为他加小括号 , 因为这里只有一个参数, 看过所有这些语法之后, 你可能想知道这些 Lambda表达式有什么用

我们还要理解一个概念即函数式接口的概念, 函数式接口, ,就是有一个抽象方法的接口, 你已经见过很多这样的函数式接口, 例如 ActionListenerComparator , 这里就可以用到 Lambda 表达式了

Arrays.sort(words,(first,second) -> first.length() - second.length());
Timert = new Timer( 1000,event ->{
     System.out.println( "At the tone,the time is " + new Date()) ;
     Toolkit.getDefaultToolkit().beep();
});

任何时候只需要一个函数式接口值, 就可以使用 Lambda 表达式 , 例如调用一个Arrays.sort , 并提供一个 Comparator 时, 为了生成一个 Comparator ,不用那么费劲的建立一个 , 不仅要实现Comparator 接口
还要有适当 compare 方法的类 , 相反你只需要拿出 compare 方法的体, 把它放在 Lambda 表达式中, 就是这样这就简短多了, 而且一旦你熟悉了就会发现这很容易读, 这里就是在说对这个数组 words 排序, 这里的比较标准是如果有两个单词, 要计算他们的长度之差, 对于计时器也是一样, 这里创建一个Timer
1,000毫秒建立一个 ActionEvent, 并把它传递到这里看到的代码, 实际上这正是设计 Lambda 表达式的初衷, 可以取一个 Lambda 表达式 , 把它保存到一个接口类型的变量中, 或者传递到一个需要接口类型值的方法, 其实这也是使用 Lambda 表达式唯一能做的事情

例如不能把 Lambda的表达式存储在一个对象变量中 , 因为对象不是函数式接口 , 另外不能只是写出一个 Lambda 表达式而不存储到任何地方, 那样他是没有用的

所以对于 Lambda 表达式你只能做一件事 , 就是把它传递到某个地方, 一个需要函数式接口实例的地方 , 函数式接口也就是只有一个方法的接口, 然后调用这个方法时 ,就会调用你的 Lambda 表达式, 也就是你在箭头后面提供的代码

在这个演示程序中, 我们将具体使用 Lambda的表达式

public static void main(String[] args) {
        String[] planets = new String[]{"Mercury", "Venus", "Earth", "Mars",
                " 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));
        Timert = new Timer(1000, event ->
                System.out.println("The time is " + new Date()));
        t.start();
        // keep program running until user selects "Ok" 保持程序运行,直到用户选择“确定”
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }

首先这里有一个 String 数组, 这里我选择的是行星名 ,首先通过调用 Arrays.sort对他们排序, 这会按字典顺序排序 , 然后再按长度对他们排序, 所以我们要再次调用 Arrays.sort, 这一次要指出我们希望以这样一种方式排序, 即比较两个字符串, 要要使用他们的长度之差进行排序, 这里我建立了一个计时器, 要求每1,000毫秒打印一个消息, 指出当前时间

所以在这里可以看到确实发生了计时器事件, 另外开始时可以看到, 利用基本 Arrays.sort , 行星名首先按字典顺序排序, 然后利用这里提供的比较函数 , 按长度排序,

public interface Predicate<T>i
    boolean test(T t);
    · · ·
}
public interface BiFunction<T, U,R>{
	R apply(T t, u u);
}

上一个例子中使用的函数式接口, Comparator ActionListener 已经在java中存在很长时间了
java8中添加 Lambda 表达式实 , 我们还得到了一个新包java.util.function, 其中包含更多泛型函数式接口, 这里有一个名为 Predicate 的接口, Predicate 会为你提供一个布尔条件, 可以为 truefalse, 参数类型为 T, 另外有一个名为 test() 的抽象方法, 这个方法检查, 所传入的这个 T 类型的参数, 会提供 true false

例如ArrayList 类有一个很有用的 removeIf 方法

list.removeIf(e -> e == null);

他接受一个 Predicate ,所以调用 list.removeIf 时可以提供一个函数, 这个函数取一个参数, 以是任意的数组元素类型 , 然后对这个元素检查某个条件 , 这里我会检查这个元素是否为 null, 所以要把这个 Lambda 表达式读作 : 给定一个 e, 如果给定了这个元素为 null, 就返回true , 把这个函数传递到removeIf 时, removeIf 就会从这个 ArrayList 中 ,删除所有 null 值, 所以这是一种非常简洁的做法
与写一个循环, 再由这个循环测试各个元素, 并调用 remove 的做法相比, 使用 Lambda表达式要简单的多
java.util.function 包, 还有很多其他非常通用的接口,

Timert = new Timer(1000,event -> System.out.println(event)) ;

例如其中有一个 biFunction 接口, 这可以是一个任意的函数, 他接受两个不同类型的参数, 并返回第三种类型的结果, 后面还会看到这样一些接口, 还有其他一些便捷语法 , 使得写 Lambda 表达式可以更为简洁
考虑这里的这个例子, 这个 Lambda 表达式接受一个事件 event, 然后他想打印这个事件
我可以用一个方法引用来写这个 Lambda 表达式

Timert = new Timer(1000,System.out::println);

就像这样, 就是 System.out ::println, 这表示这是 System.out 对象的 println 方法, 当然这个println 方法有一个参数, 这里可以更清楚的看到这个参数就是这里的 event, 不过把它写为方法表达
式时,都知道有这个参数
再来看另外一个例子

Arrays.sort(words, string:: compareToIgnoreCase)

String::compareToIgnoreCase , 这是 String类的 compareToIgnoreCase方法, 它有两个参数
就是要比较的两个 String, Arrays.sort就需要这样一个比较方法, 他要接受两个值然后返回一个整数, 所以可以把这个方法引用, 传递到 Arrays.sort, 结果是这个 word 将排序, 不过这个排序会忽略大小写
所以这是指定这个工作的一种超级简洁的做法

方法引用有三种形式:

可以有一个对象::加一个实例方法 system.out 是一个对象 println 是一个实例方法, 然后在这个对象上调用这个实例方法, 方法的参数是 Lambda 表达式的参数

还可以有类::静态方法, 这里没有给出这种形式的例子, 不过这表示你想调用这个类的静态方法, 并提供他需要的参数

还有一种形式是类::实例方法, 在这个例子中可以看到, 在这里其中一个参数是隐式的, 或者接收者参数
另一个参数是方法的显示参数,对于 Lambda 表达式, 另一个可以使用的快捷方式是构造器引用. 构造器引用总是以::new 结尾, 这个 new 就表示构造器, 所以举个例子 Person::new 是一个 Person 构造器的引用, 也可以把它写为 s -> new Person(s), 这是一样的, 哪个构造器?编译器会通过通常的重载解析来确定, 取决于你在什么上下文使用, 他会知道你想提供什么参数, 然后选择适当的构造器, 或者如果没有匹配的构造器, 会给出一个错误, 下面用一个例子来说明, 为什么这样一个构造器引用很有用

ArrayList<String> names = ...;
Stream<Person> Stream = names.Stream().map(Person::new);
List<Person> people = Stream.collect(Collectors.toList());

假设有一个包含名字的 ArrayList名为 names, 我想由这个 ArrayList 建立一个 Person 列表
List<Person>, 要用这些名字构造列表中的各个 Person, 当然我也可以写一个循环, 可以说对于数组 names 中的每一个名字, 构造一个新 Person然后把它们放在另一个列表中, 当然人们以前总是习惯写这样的代码, 不过现在采用一种函数式思维, java类库提供了一些功能可以让你完成这些工作, 而根本无需任何循环, 我们现在不会深入讨论具体的细节 ,这里只是让你有所认识, 你要做的就是得到集合names 指出我想把它作为一个 Stream, 也就是流, 之所以想要一个 Stream, 原因是 Stream 接口有一个很棒的方法 map, map 所做的是接受一个 Lambda表达式, 然后把这个表达式应用到这个流中的各个元素, 所以首先有一个 String的流, 通过映射 Person::new, 会对各个 String应用这个构造器, 现在我就有了一个 Stream<Person>, 也就是 Person 的流, 现在必须把这个 Stream监控号 Person, 再转换回一个List<Person>也就是 Person 列表

可以看到, 这个工作在这里的第三行代码中完成, 我不想详细分析这行代码, 当然理想情况下, ArrayList 本身应该有这样一个 map 方法, 那样我们就不需要”取道” Stream了, 不过现在我们还做不到, 构造器引用还有一个有些晦涩的用例用来处理数组

数组是类, 所以 int[]::new是一个合法的表达式, 这是什么意思, 对于数组构造器, 你要指定数组的长度, 所以这等同于这个表达式, 接受一个整数并返回一个新的给定长度的 int 数组, java中有一个让人有些奇怪的泛型类型限制, 构造器引用对于克服这个限制很有用

在java中构造任意类型的数组是不合法的, 第八章还会介绍有关的一些细节, 如果你想建立一个给定类型的数组, 就需要采用其他办法

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

在这里数组构造器应用就能提供帮助, 所以举个例子, 如果我有一个 Person 流, 也就是 Stream<Person>, 我想把它转换为一个 Person 数组, 可以使用 toArray 方法, 传入一个 Person[]::new, 这样一来, toArray 方法可以使用传入的这个构造器引用, 返回一个 Person
对象受阻, Lambda 表达式很重要的一个方面, 是他们可以访问外围作用域的变量

来看这里的例子

public static void repeatMessage(String text, int delay){
    ActionListener listener = event ->{
        system.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay,listener).start();
}

这里有一个方法 repeatMessage, 他有两个输入, 一个文本 text 和一个延迟 delay, 然后由这个 Lambda表达式, 构造一个监听器对象, 这是一个 ActionListener, Lambda表达式打印给定的文本, 发出毕的一声, 然后用给定的延迟和这个监听器创建一个新的计时器, 再在这个计时器上调用 start

repeatMessage( "Hello",1000); // Prints Hello every 1,000 milliseconds

使得每格 delay 指定的时间单位为毫秒, 就会打印这个文本, 现在假设我们有一个 repeatMessage
调用文本是 hello, 延迟为1,000, 注意这里监听器引用了 text 变量, 这是完全合法的, 因为 text 是在外围作用域中定义的, 从这个块来看, 可以看到 text, 这说明可以使用这个变量, 不过实际上想想看, 如果沿着动作链来看, 这有些奇怪, 调用 repeatMessage, 所以 text delay 传递到 repeatMessage完成他的工作, 计时器启动

现在 repeatMessage 退出一个方法, 退出时他的所有局部变量都会消失, 所以现在 text 已经没有了1,000毫秒之后调用监听器, 他要引用 text, 这还能正常工作, 由此可以得出, Lambda表达式中, 不只是有方法代码肯定还有些什么

确实如此, Lambda表达式由三部分组成, 在这里就是大括号里的代码或者更一般的箭头后面的代码, 参数
也就是前面的内容, 第三部分是 Lambda 表达式体中使用了, 但是实际上并没有在 Lambda 表达式中定义的所有变量的值, 在这里就是 text. 因为所有其他内容都已经定义了, 这里的 text 被我们称为自由变量, 自由变量就是没有在 Lambda 表达式中定义的变量, Lambda表达式会把这个 text的值藏起来

创建这个 Lambda表达式时, 他知道以后需要这个 text的值, 所以会把它放在某个地方, 实际上 Lambda表达式在虚拟机中实现为一个对象, 所以他会把他需要的东西放在实例变量里, 要使用 Lambda的表达式
其实并不需要知道这些实现细节, 不过你会发现, 如果知道他如何工作使用时会更容易

顺便说一句, 对于这样一个组合, 也就是代码块和参数以及自由变量的值, 这个组合有一个术语叫闭包
所以 Lambda表达式提供了java中的闭包

捕获外围作用域中的变量, 有一个重要的限制, 这就是你只能捕获不会改变的变量, 下面来看一个例子

public static void countDown(int start, int delay){
    ActionListener listener = event >{
    	start--; // Error: Can't mutate captured variable
        System.out.println(start);
	};
	new Timer(delay,listener).start( );
}

这里有一个 countDown 方法, 他为计时器接受一个整数 start和一个延迟 delay, 这里我想做的是在监听器中我想让 start 递减, 现在他是一个捕获的变量, 他来自外围作用域, 这在java中是不合法的, Lambda表达式中不能更改一个捕获的变量, 如果你捕获的变量会在Lambda表达式之外改变这也是不合法的

public static void repeat(String text, int count){
for (int i = l; i <= count; i++){
ActionListener listener = event ->
System.out.println(i + ": " + text); // Error: Cannot refer to changing i
    new Timer( 1000,listener ).start() ;
	}
}

这里的设置稍有不同, 这里有一个 for 循环, 在这个 for 循环中, 出于某种原因我要为每一次迭代建立一个 ActionListener, 在这里我想引用变量 i, 它会在外围作用域中改变, 而你不能引用一个变化的 i

Lambda表达式中, 能引用的只有实际上为 final的变量, 这说明不论是在 Lambda表达式体中, 还是在 Lambda表达式之外, 这个变量都不会改变

到目前为止, 你已经了解了如何使用Lambda表达式向其他函数, 或者说向其他服务传递功能, 下面反过来
想象一下服务提供者会是什么样, 就是要提供一个服务并接受 Lambda的表达式

repeat(10,() -> System.out.println( "Hello,world!"));

我想写一个名为 repeat 的方法, 它有两个参数, 一个是重复次数也就是要重复多少次, 另一个是某种动作
这是一个无参数但有一个体的 Lambda 表达式

repeat 的任务应当是, 按照第一个参数所说的把传递到这里的这个动作, 重复指定的次数, 这里可以看到这个方法的实现

public static void repeat(int n,Runnableaction){
	for (int i = 0; i < n; i++) action.run( );
}

repeat 有两个参数, 第一个是一个整数, 第二个必须是某种函数式接口, 所以现在我需要找到一个合适的函数式接口, 能接受类似这样的一个 Lambda表达式, 实际上java API 有一个接口 Runnable, 它有一个抽象方法名为 run, 恰好能满足这里的要求, 那么现在我要做什么呢

我有一个循环, 因为我想使用这么多次也就是 n 次, 我想把传递给我的动作执行 n 次, 这个动作是一个 Runnable实例, 所以我要调用他的 run方法

注意提供这个功能的代码, 并没有使用 Lambda 表达式, 他只使用了接口, 实际上可以用一个采用传统方式创建的, Runnable调用这个方法, 即建立一个实现了 Runnable接口的类
或者可以直接传入一个 Lambda表达式来调用, 在当今时代, 可能大多数人都会采用这种方式, 这是一个非常简单的例子, 这个动作只是重复一定的次数

public static void repeat(int n,IntConsumer action){
	for (int i = 0; i < n; i++) action.accept(i);
}

不过举例来说, 如果我想在这个动作中, 访问这里的变量 i, 从而能够倒数计数, 该怎么做呢, 不是将还来偶尔的打印10次, 我可能想打印一个倒数的数, 9、8、7等等直到0
在这种情况下, 我就需要一种办法, 能够在 Lambda表达式体中, 引用这个计数器的值, 所以他要作为一个参数传入, 就是这里, 这说明我需要改变 action 参数的参数类型, 以前他是一个 Runnable要执行他, 只需要不带参数的调用 rum 方法

现在需要把这里的计数器, 传递到接口的这个方法, 实际上java.util.function 包中, 有一个名为 intConsumer 的接口, 正好可以完成这个工作, 他有一个名为 accept 的方法, 这个方法接受一个整数
所以这里选择了 intConsumer 作为 action参数的适当的参数类型

通过这个例子, 你应该对如何提供一个服务有所认识, 程序员可以利用一个 Lambda表达式调用这个服务, 来得到服务提供的功能

在关于 Lambda表达式的这一节的最后, 我想再谈谈 Comparator, Comparator 接口有很多有用的方法, 来创建和组合比较器, 使用这些方法可以让你对函数式编程有所认识

了解如何管理函数来创建新的函数, 这里有一个静态方法 Comparator.comparing

Arrays.sort(people,Comparator.comparing(Person::getName)) ;

他会生成一个比较器, 他的做法是你提供一个函数, 这个函数接受一个对象并从中抽取一个键, 这里我们传入函数 Person::getName, 这个函数从一个 Person 得到名字 name

现在 comparison 的工作是, 给定两个 Person抽取出他们的名字然后进行比较, 所以结果是这个数组将按名字排序

如果我传入一个不同的函数, 比如说 Comparator.comparing(Person::id), 就会得到一个不同的比较器
id 比较, 这就提供了一种非常简洁的表示比较器的方法, 这在实际中非常有用

因为像按名字比较, 按 id 比较, 按工资比较等所有这些事情, 现在都能用这种非常简单的方式完成, 甚至无需写 Lambda 表达式或实现Comparator 接口

Arrays.sort(words,Comparator.comparingInt(String::length)) ;

还有一个很小的技术问题, 如果抽取键的函数返回一个整数, 使用 comparianInt而不是 comparing 会更高效, 这样可以少一个装箱的步骤, 这里所做的就是按单词的长度比较单词, 抽取键的函数是 length, 会用这个标准来比较单词

这一讲前面使用了Lambda的表达式来完成这个比较, 而这比前面看到的那种做法还要简单, 比较复杂的对象时, 通常想打破平局, 使用一个标准比较时可能会得到相等的结果, 在这种情况下, 可能就要考虑使用辅助键或次键, 同样的Comparator类会让这个工作变得非常简单

有一个方法 thenComparing, 他会修改一个比较器, 指出如果这个比较器返回0, 我们就传递第二个比较器, 这是一个很典型的例子

Arrays.sort(people,
	Comparator.comparing(Person::getLastName)
		.thenComparing(Person::getFirstName));

我们要对 people 排序, 这里指出建立一个主比较器getLastName, 他会按姓来比较 Person, 如果这个比较失败, 再比较名

如果按传统方式写这个代码, 很可能需要10行以上的代码, 所以从这里可以看到, 基于 Lambda的表达式或方法表达式, 函数式编程的一个好处是, 你可以写管理函数的函数, 可以采用某种方式调整和组合那些函数


lambda是函数式接口,根据其原理,只声明了一个抽象方法的接口,可以有多个静态方法、默认方法。

lambda的本质是匿名内部类


Lambda表达式精简语法

例子:

ifOne one=(int a)->{
    return 10;
}

语法注意点∶

  1. 参数类型可以省略 效果:one=(a)->{ return 10;}
  2. 假如只有一个参数 , () 括号可以省略 效果: one=a->{ return 10;}
  3. 如果方法体只有一条语句,{}大括号可以省略 效果:one=a->{ return 10;}
  4. 如果方法体中唯一的语句是return返回语句,那省略大括号的同时return也要省略 效果:one=a->10

6.0理解内部类的工作原理

从概念上讲内部类非常简单, 就是定义在另一个类中的一个类

Lambda表达式出现之前, 通常都使用内部类来实现回调, 所以你还会在以前的遗留代码中看到内部类,
他们还有另外一些用途, 如隐藏实现细节, 下面给出一个内部类的例子

public class TalkingClock{
	private int interval;private boolean beep;
	public Talkingclock(int interval, boolean beep) { . . . }
    public void start() { . . . }
	public class TimePrinter implements ActionListener{ // an inner class
	...
    }
}

这里有一个外部类, 这就是一个普通的类, 名为 TalkingClock, 在这个类中还有另一个类名为 TimePrinter, 现在如果你有一个 TalkingClock 对象, 其中并不包含 TimePrinter, 这里的嵌套只表示 TimePrinter, 在 TalkingClock 类中定义

如果你想引用这个类, 可以写为 TalkingClock.TimePrinter, 下面来看 TimePrinter 的实现

public class TimePrinter implements ActionListener{
    public void actionPerformed(ActionEvent event){
    	system.out.println("At the tone,the time is " + new Date());
        if (beep) Toolkit.getDefaultToolkit().beep();
    }
}

他实现了 ActionListener 接口并且实现了 actionPerform 的方法, 前面已经见过, 这会打印一个消息
指出现在的时间, 这里有一个小花样, 只有当设置了这里的beep变量时才会发出逼的一声, 但是哪个beep不变量呢?
这里并没有任何 beep 变量, 不过如果返回到上一页, 查看 TalkingClock 类的定义, 可以看到他有一个boolean类型的 beep 实例变量, 所以那就是这里引用的 beep

因此可以看到, 内部类可以访问外部类的实例变量, 外部类的实例变量? 这是什么意思?外部类的哪个实例
具体来讲, 就是创建这个内部类对象的那个实例

所以当一个外部类对象创建一个内部类对象时, 这个内部类对象实际上会记住是谁创建了他, 并且有那个对象的一个引用, 通过这个引用, 他能看到创建他的外部类对象的实例字段

在这个示例程序中

package com.horstmann.test;

import jdk.nashorn.internal.scripts.JO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

public class InnerClassTest {

    public static void main(String[] args) {
        TalkingClock clock = new TalkingClock(1000, true);
        clock.start();
        //keep program running until user elects "ok" 保持程序运行,直到用户选择“确定”
        JOptionPane.showMessageDialog(null,"Quit program");
        System.exit(0);
    }
    static class TalkingClock {
        private int interval;
        private boolean beep;
        /**
         * Constructs a talking clock
         * @param interval the interval between messages (in milliseconds)
         * @param beep     true if the clock should bee
         */
        public TalkingClock(int interval, boolean beep) {
            this.interval = interval;
            this.beep = beep;
        }
        /* Starts the clock.*/
        public void start() {
            ActionListener listener = new TimePrinter();
            Timer t = new Timer(interval, listener);
            t.start();
        }
        //内部类
        public class TimePrinter implements ActionListener {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone,the time is " + new Date());
                if (beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        }
    }
}

可以看到 TalkingClock 类有两个实例变量, 这是他的构造器, 这是他的 start 的方法, 这会创建 Timer, 这里 Timer的回调是一个 TimePrinter 实例, 这个 TimePrinter 是我们的内部类, 这就是这个内部类, 可以看到这里的 beep 变量并不是引用 TimePrinter 中定义的任何内容, 而是引用这里的这个实例变量
运行这个程序时会得到与之前例子相同的消息

现在来介绍一些语法, 你已经看到了内部类引用外部类的一个变量,

if (Talkingclock.this.beep)...

可以用这种 this语法更清楚的表明这一点, 不过这与以前的 this 与法有些不同, 首先是外围类即:外部类的类名, .this 然后是实例字段 , 所以如果在内部类中使用 TalkingClock.this.beep 这表示 this , 即:当前对象的这个 beep 实例变量来自 TalkingClock , 另外我在前面提到过, 如果有一个非私有的内部类, 任何人都能通过 outerClass.InnerClass 的形式引用这个内部类
在我们的例子中, 这就是 TalkingClock.TimePrinter, 大多数情况下创建一个内部类对象时, 都是由外部类的某个方法来完成的, 这个方法的隐式参数, 就是生成这个内部类对象的对象

不过实际上, 外部类的任何对象都能生成一个内部类对象, 语法如下所示:外部类对象.new然后是内部类名

Talkingclock jabberer = . . ;
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

在这里所生成的内部类对象会记住他是, jabberer创建的, 所以 ActionListener 方法中的 beep 应用
就会指向 jabberer.beep 你看到的 TimePrinter类, 实际上只在一个方法中用到, 就是启动 TalkingClockstart 方法

public void start(){
	class TimePrinter implements ActionListener{
        public void actionPerformed (ActionEvent event){
        System.out.println("At the tone,the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    }
	ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval,listener) ;
    t.start();
}

在类似这样的情况下, 可以把这个内部类的整个代码移到这个方法中, 就像这里看到的一样, 为什么要这么做?这样做的最终目的是隐藏, 这样一来任何其他人都不能访问这个类, 这个类会完全成为这个方法的局部类, 所以你不用担心命名冲突之类的问题, 局部类不能声明为 publicprivate protected, 因为他在这个方法之外是不能访问的

类似于 Lambda 表达式, 局部类的代码不只是能访问实例变量, 还能访问局部变量, 前提是这些局部变量实际上是final变量, 所以在这里可以把这个 start, 改为下面看到的这个签名

public void start(int interval, boolean beep)

这里有两个参数 interval beep, 如此一来这里看到的 beep 会引用这个参数, 这没问题因为它是 final 变量, 这个参数不会改变, 实际上还可以更进一步

public void start(int interval, boolean beep){
    ActionListener listener = new ActionListener(){
        public void actionPerformed(ActionEvent event){
            System.out.println("At the tone,the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    };
	Timer t = new Timer(interval, listener);
    t.start() ;
}

如果一个局部类只实例化一次就像我们的例子中一样 , 我只建立了 TimePrinter 的一个实例, 那么甚至不用为他提供一个名字, 可以建立一个匿名类, 就像这样new ActionListener(), 这表示我的监听器应该是某个实现了 ActionListener 接口的类的实例, 过我不用为这个类提供名字, 我想要的这个没有名字的类, 应当有一个类似这样的, actionPerformed 的方法

General syntax: //一般语法
new SuperType(construction parameters){
	inner class methods and fields
}

匿名类的一般语法如下, 首先是 new 超类型的名字, 这个超类型可能是要扩展的一个类或者是你要实现的一个接口, 后面是构造参数放在小括号里, 对于一个接口, 这总是一对空括号, 后面是大括号, 然后是你想要的方法或字段, 在这里我们只想要一个方法

不过完全可以有更复杂的匿名内部类 , 其中包含多个方法以及字段, 以前要指定一个事件接听器或者另一个回调, 这就是最好的方法

如今我们有了Lambda表达式, 你不会再用匿名内部类来实现只有一个方法的接口, Lambda表达式要好的多, 不过如果需要实现一个包含多个方法的接口, 或者某个类只需要扩展一次, 那么可能还会使用内部类

这个示例程序显示了前面给出的 start 的方法

public void start(int interval, boolean beep) {
    ActionListener listener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent event) {
            System.out.println("At the tone,the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    };
    Timer t = new Timer(interval, listener);
    t.start();
}

这个 start 的方法有两个参数 , 时钟间隔 interval boolean类型的 beep, 这表示是否要响一声
这里ActionListener(){...}是我的匿名类不类, 他是这个方法的局部类, 可以看到这个类没有名字
ActionListener 是接口名, 所以这就表示他是一个实现了 ActionListener 接口, 而且没有构造参数的新类, 他的 actionPerform 的方法应该是这样, 这个方法引用了 beep, 这是一个局部变量这是可以的, 因为他实际上是 final变量

顺便说一句, 在老版本的java中必须真正把这些变量声明为 final, 但是在java8中不再需要这么做, 关于内部类还有最后一个难点, 内部类有一个变种叫静态内部类

静态内部类是一个没有外部对象引用的内部类

如果你有一个类不需要访问外部对象的任何实例变量或方法, 静态内部类就很有用, 通过把它声明为 static, 可以避免引用外部对象, 这里给出使用一个静态内部类的例子

class ArrayAlg{
    public static class Pair{
    	public double first;
        public double second;
    }
    ...
    public static Pair minmax(double[] values){
        ...
        return new Pair(min,max) ; // no creating object
    }
}

这里我想要一个Pair类, 他只是把两个值聚集在一起, 我把它命名为 firstsecond, 之所以想要这么做, 原因是我想实现一个返回两个值的方法, 我要计算一个数组的最小值和最大值, 这是两个元素, 我无法一次返回两个 Double, 所以必须把它们打包在一起, 当然我也可以把它们包装在一个链接数组中, 不过我这里选择把它们放在 Pair中,

这样只要有人得到这个答案, 就可以用 p.first , 和 p.second 的来引用

这只是一个非常简单的类, 我不关心封装或其他方面 ,这里大胆的把这些字段声明为 public, 不过我还是希望Pair的作用域仅限于ArrayAlg内部

因为Pair是一个很常用的名字, 其他人可能也会使用这个名字, 这样一来可以把它叫 ArrayAlg.pair

ArrayAlg.Pair p = ArrayAlg.minmax(data);

这个类甚至没有任何方法, 所以显然不需要外部类的引用, 因此我把它声明为 static, 静态内部类的概念很有策略性, 你会发现他作为一些类的实现细节, 这些类处于某种原因, 可能需要有一些小的辅助类, 在这里的 ArrayAlg 类中, 可以看到内部类Pair定义为一个静态内部类

package com.horstmann.test;

public class StaticInnerClassTest {

    public static void main(String[] args) {
        double[] doubles = new double[20];
        for (int i = 0; i < doubles.length; i++) {
            doubles[i] = 100 * Math.random();
        }
        ArrayAlg.Pair minmax = ArrayAlg.minmax(doubles);
        System.out.println("min = " + minmax.getFirst());
        System.out.println("max = " + minmax.getSecond());
    }

     static class ArrayAlg {

        public static class Pair {
            //A pair of floating-point numbers     一对浮点数
            public double first;
            public double second;

            /**
             * Constructs a pair from two floating-point numbers    用两个浮点数构造一对
             * @param first  the first number
             * @param second the second number
             */
            public Pair(double first, double second) {
                this.first = first;
                this.second = second;
            }


            public double getFirst() {
                return first;
            }

            public double getSecond() {
                return second;
            }
        }

        public static Pair minmax(double[] values) {

            double min = Double.POSITIVE_INFINITY;
            double max = Double.NEGATIVE_INFINITY;
            for (double v : values) {
                if (min > v) {
                    min = v;
                }
                if (max < v) {
                    max = v;
                }
            }
            return new Pair(min, max);
        }
    }
}
output:
min = 14.808445887598154
max = 96.88729420769607

这里可以看到 main max 方法, 他会为存入的数组, 计算最小值和最大值, 然后把这两个值打包到Pair中并返回, 这里可以看到如何调用这个方法, 我要计算一个数组的最小值和最大值, 这个数组中填入了随机数
我会得到一个 pear , p.firstp.second分别是最小值和最大值

运行这个程序时, 我得到了最小值和最大值, 看起来很合理, 现在如果我把这个类声明为一个普通的内部类
就像这样删除public static class Pairstatic , 会发生什么呢?

这样一来当我构造一个新Pair时, 会遇到一个问题, 这个问题出现在静态方法中, 根本没有这个指针, 在一个普通的内部类中, 我们可能想把外部类的引用, 设置为这个指针, 但是并没有这样一个指针, 当然这里的补救方法, 就是把它声明为一个静态内部类, 这样就一切正常了

介绍完这个有些技术性的概念, 我们对内部类的讨论就告一段落了

正如我说过的, 在java8中, 内部类的很多用例, 已经被 Lambda表达式所取代, 不过偶尔你还会看到内部类用于其他一些用途, 如限定类的作用域和信息隐藏, 这是很长也很复杂的一课, 我们学习了接口和 Lambdm的表达式

使用java编程时, 这些非常有用, 下一课我们将转而介绍异常处理, 更一般的来说, 我们会研究如何处理出问题的程序


匿名内部类的特点:

  1. 匿名内部类没有类名,它也就不能定义出构造器,所以它没有自己的构造器。
  2. 匿名内部类是隐式地继承了一个特定的类,或者隐式地实现了一个特定的接口。
  3. 匿名内部类无法定义静态成员和静态方法。
  4. 匿名内部类不能用public,protected,private,static修饰。
  5. 匿名内部类只能创建一个实例,它的本身就是一个实例。

匿名内部类的格式:

  • 必须是类或者接口 即: 实现一个类或者实现一个接口
  • 接口必须是一个函数式接口,即: 只有一个抽象方法

格式:

new 类名/接口名(){
    重写抽象方法
}
  • 如果是类的话,相当于子类的实现类

    • Animal是一个父类,eat是他的抽象类

    • 创建之后,只是相当于创建子类对象对抽象方法进行了重新,并不会立即执行

    • 第一种调用方法

    • //整体就等效于:是Animal父类的子类对象
      new Animal(){
          @Override
          public void eat() {
              System.out.println("狗吃肉");
          }
      }.eat(); 	//通过子类对象调用方法
      
    • 第二种调用方法 用父类的引用作接收了子类对象——多态的体现形式

    • Animal a = new Animal(){
          @Override
          public void eat() {
          System.out.println("狗吃肉");
          }
      }
      a.eat();
      
  • 如果是接口的话,相当于接口的实现类

    • 接口不能被实例化,只能通过多态的形式,让子类实例化

    • Inter是一个接口名

    • new Inter传递了一个实现类, 作为方法的参数传递

    • //调用方法
      function(
              new Inter({   //实现接口,并作为参数传递
              @Override
              public void method() i
                  system.out.println("我是重写后的method方法");
              }
          }.method();
      )
      
    • //接口
      public interfact Inter(){
          void method();
      }
      //方法
      public static void function(Inter i) {
      	i.method();
      }
      

方法

方法名称的命名规则和变量一样使用小驼峰
方法体:也就是【大括号】,其中可以包含任意条语句 例句:public static void 方法名(){方法体}

注意事项:
A。方法定义的先后顺序无所谓。
B。方法定义不能产生嵌套包含关系。 即类中不能嵌套类
C。方法定义好了之后,不会自动执行。若需要执行,则需进行方法的调用

调用方法的格式: 方法名称();

对于byte/short/char三种类型来说如果右赋值的数值没有超过范围,那么Java编译将会自动隐含的为我们补上一个(byte)(short)(char)

1,如果没有超过左侧的范围,编译会补上转换
2,如果右侧超过了左侧范围,那么直接报错。

【编译*的常量优化】 (表达式当中有变量就不能)
在给变量 赋值的时候,如果右侧的表达式当中都是常量,没有任何变量,那么编译Java将会直接将若干个常量表达式计算得到结果。

封装;

封装性在Java中的体现;1,方法就是一种封装。2,关键字private也是一种封装。
封装就是将一些细节隐藏起来,对于外界不可见。

问题,定义一个方法内的某参数时无法,无法阻止不合理的数值被设置进来。

private—私有化

解决办法 private—私有化 格式:private 数据类型 参数值;
一旦使用private进行修饰,那么本类当中仍然可以随意访问,但是一旦超出了本类范围之外就不能再直接访问了。
无法直接访问,但可以间接访问。定义一对Getter/Steeer方法 (目的在赋值的方法中添加选择语句来对赋值的参数进行限制)
必须叫setXXX或者是getXXX命名规则
!对于基本数据类型当中的Boolean值,Getter方法一定要写成isXXX的形式,而SetXXX规则不变。
对于Getter来说,不能有参数,返回值类型和成员变量对应;
对于Setter来水,不能有返回值,参数类型和成员变量对应;

当方法的局部变量和类的成员变量重名的时候,根据“就近原则”优先使用局部变量。(实际上是局部变量后赋值)
如果需要访问本类当中的成员变量,需要使用格式: this.成员变量名
“通过谁调用的方法,谁就是this“ this实质上就是 调用地址

构造方法是专门用来创建对象的方法,当我们通过关键字new来创建对象时,其实就是在调用构造方法。
格式: public 类名称(参数类型 参数名称){
方法体;
}
注意事项:
A,构造方法的名称必须和所在类的名称完全一样,连大小写也要一样
B,构造方法不要写返回值类型,连void也不要写
C,构造方法不能return一个具体的返回值
D,如果没有编写任何构造方法,那么编译器将会默认赠送一个构造方法,没有参数、方法体什么事情都不做。 public Studnt(){}
E,一旦编写了至少一个构造方法,那么编译器将不再赠送。
F,构造方法也是可以进行重载的。 重载:方法名称相同,参数列表不同。

Java Bean

定义一个标准的类: 这样标准的类也叫Java Bean
1,所有的成员变量都要使用private关键字修饰。
2,为每一个成员变量编写一对Getter/Setter方法
3,编写一个无参数的构造方法
4,编写一个全参数的构造方法

API:应用程序编程接口 Java api 是一本程序员的字典,是JDK提供给我们使用的说明文档。

匿名对象

就是只有右边的对象,没有左边的名字和赋值运算符。
格式: new 类名称;
注意事项:匿名对象只能使用唯一的一次,下次再用不得不再创建一个新对象。
使用建议;如果确定有一个对象只需要使用唯一的一次,就可以用匿名对象。
(每new一次都是一个新地址)
匿名对象使用在,只需输入一次的输入或者打印。
举例:调用是打印是

public  static  void     方法名( Scanner  sc){
int   num    =   sc.nextInt();
System.out.printIn();
}

调用从键盘输入的是

public  static    Scanner  方法名{
return  new  Sanner (System.in);
}

生成随机数Random

A,导包 import Java.util.Random
B,创建 Random r = new Random(); 小括号当中为空
C,使用 获取一个int随机数,范围为int的所有范围,有正负

int  num  = r.nextInt();

获取一个随机的int数字,参数代表了范围,左闭右开区间 [ ) 从零开始

集合(泛型)

数组可以用来存储一个对象;但是是数组一旦创建就不能改变长度。于是引出集合的概念
ArrayList集合的长度是可以改变的。
对于ArrayList有一个尖括号代表泛型。取自Element(元素)的首字母。在出现的地方,我们使用一种引用数据类型将其替换即可,表示我们将储存那种数据类型
泛型,也就是装在集合当中的元素,全都是统一的什么类型。
注意!:泛型只能是引用类型,不能是基本类型。
如果希望向集合ArrayList当中储存基本类型数据,必须使用基本类型对应的”包装类”
基本类型 包装类(引用类型,包装类都位于Java.lang包下)
byte Byte
short Short
int Integer [特殊]
long Long
float Float
double Double
char Character [特殊]
boolean Boolean
从JDK1.5+开始,支持自动装箱丶自动拆箱
自动装箱:基本类型–>包装类型
自动拆箱:包装类型–>基本类型

Arraylist数组的长度是可以变化的。
构造方法 public ArrayList() :构造一个内容为空的集合。
基本格式:ArrayList list = Aerraylist(); 在JDK7后,右侧泛型的尖括号可以留空但是<>仍是要写的。
简化格式:ArrayList
A,导包 Java.util.ArrayList (idea会自动导包)
B,创建
Arraylist的常用方法
A<public boolean add(E e) 向数组当中添加元素参数的类型和泛型一致
PS:对于ArrayList集合来说,add添加动作一定是成功的,所以返回值可用可不用.但是对于其他集合来说,add添加不一定成功.
参考代码

//向元素中添加"张三"   并检验是否成功
 boolean  success = list.add("张三");
System.out.println(list);     //输出  [张三]
System.out.println("添加的动作是否成功"+success);   //输出   添加加的动作是否成功 true

B<public E get(int index);从集合当中获取元素,参数是索引编号,返回值就是对应位置的元素.
参考代码:

//从集合中获取元素:get.  索引值从0开始.
Stringname = list.get(2);
System.out.println("第二号索引位置"+name)   

C<public E remove(int index):从集合当中删除元素,参数是索引编号,返回值就是被删除的元素.
参考代码:

//从集合中删除元素:remove.  索引值从0开始.
StringwhoRemoved = list.remove(3);
System.out.println("被删除的人是:"+whoRemoved);   //  输出被删除的元素
System.out.println(list);     //输出 list 数组中剩下的元素

D<public int size():获取集合的尺寸长度,返回值是集合中包含的元素个数.
参考代码:

int size = list.size();
Sysrem.out.println("集合的长度是:"+size);   //输出元素的长度,以int类型

向集合中添加元素: list.add(“参数”) ,该参数的类型已经由创建时输入的类型确定
注意事项:对于ArrayList来说直接打印显示的不是地址而是内容,如果内容为空,则显示的是[].

字符串的认知;
Java程序中所有的双引号字符串都是String类的对象.
字符串是常量,确定后不能更改. [重点]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
因为字符串不可改变,所以字符串是可以共享使用的. ps:字符串放置在公共的”库”中
字符串效果上相当于char[]字符数组,但是底层原理是byte[]数组.
字符串常量池,:程序当中直接写上的双引号字符串,就在字符串常量池.
创建字符串的常见方式;
三种构造放法:
1, public String(); //创建一个空白字符串,不含有任何内容.
2,public String(char[] array); //根据字符数组的内容,来创建对应的字符串.
3,public String(char[] array); //根据字节数组的内容,来创建对应的字符串.
一种直接创建:
Stringstr = “Hello”
注意:直接写上双引号,就是字符串对象. 无论是否new

字符串的比较:
对于基本类型来说,是进行数值的比较.
对于引用类型来说,
是进行地址值的比较.
对于字符串的比较应该使用;equals方法
public boolean equals(Object obj):参数可以是任何对象,只有参数是一个字符串并且内容相同的才会给true;否则返回false.
备注:任何对象都能用Object进行接收
参考代码:

"hello".equals(参数名);
或者 system.out.println(str.equals(str2));

ps:
A,区分大小写,不区分大小写的需要用equalsIgnulleCase来替代equals
B,equals,方法具有对称性,两个参数位置放前后效果一样.
C,推荐写法,[常量]字符串写前面. 否则容易出现空指针异常.

不区分大小写的需要用equalsIgnulleCase
方法:public boolean equalsIgnulleCase(Stringstr)
ps:不区分的范围仅限于英文大小写


两种实例化方式的区别?
那么问题来了,我们为什么不直接使用 ArrayList list=new ArrayList();的方式来实例化ArrayList对象,并调用其中的方法,而有时候大部分会通过List list=new ArrayList();来实例化对象呢?
原因有二:
第一,两种实例化的方式由所区别,第一种方式创建的对象继承了ArrayList的所有属性和方法。 而第二种方式创建的对象只能调用List接口中的方法,不能调用ArrayList中的方法。
第二、使用第二种方式创建的对象更为方便。List接口有多个实现类,现在用的是ArrayList,如果要将其更换成其它的实现类,如 LinkedList或者Vector等等,这时只需要改变这一行就行了: List list = new LinkedList(); 其它使用了list地方的代码根本不需要改动。 在编写代码时,较为方便。

List是一个接口,而ArrayList是List接口的一个实现类。

​ ArrayList类是继承AbstractList抽象类和实现List接口的一个实现类。

​ 因此,List接口不能被构造,也就是我们说的不能创建实例对象,但是我们可以像下面那样为List接口创建一个指

向自己的对象引用,而ArrayList实现类的实例对象就在这充当了这个指向List接口的对象引用。


字符串的常用方法:

A,public int length(); //获取字符串当中含有的字符个数,拿到字符串长度.
参考代码:

  int length = "fsjfjvorgrggg".length();

B,public Stringconcat(Stringstr): //将当前字符串和参数字符拼接成为返回值新的字符串.
参考代码:

str3  =str.concat(str2);   

!拼接是创建了一个新的字符串
C,public char charAt(int index); //获取指定索引位置的单个字符(索引从0开始)
参考代码:

char ch = "hello".charAt(1);

D,public int indxOf(Stringstr); //查找参数字符串在本字符串当中首次出现的索引位置,如果没有返回-1值.
参考代码:

Stringoriginal = "helloWorld";
int index = original.indexOf("llo");
System.out.println("第一次索引值是;"+index);    //输出  在第一次索引位置的字符是:2

截取字符串:

A< public Stringsubstring(int index);//截取从参数位置一直到字符串末尾,返回新字符串.
参考代码: Stringstr1=”helloWorld”;
Stringstr2= str1.substring(5);
String.out,println(str2); //输出hello
B< public Stringsubstring(int index,int end); //截取从begin开始,一直到end结束,中间的字符.
备注:[begin,end),包含左边,不包含右边.

字符串转换;

public char[] toCharArray(); //将当前字符串拆分成为字符组作为返回值.
参考代码:

char[] chars = "hello".toCharArray();    //转换成为字符数组

public byte[] getBytes(); //获得当前字符串底层的字节数组
参考代码:

byte[] bytes = "abc".getBytes();
System.out.println(bytes[0]);    //输出97   ASCII中a的数值为97

public Stringreplace(CharSequence oldString,CharSequence newSteing);
参考代码:

Stringstr1 = "How do you do?";
Stringstr2 = str1.replace("o","*");    //将how do you do中的"o"换成"o"

//将所有出现的老字符串替换成为新的字符串,返回替换之后的结果新字符串.

分隔字符串的方法:

public String[] split(Stringregex); //按照参数的规则,将字符串切分为若干部分.
参考代码: Stringstr1 = “aaa,bbb,ccc”;
Sting[] array1 = str1.split(“,”); //以逗号为规则将字符串分隔
注意事项;split方法的参数实际上是正则表达式. 不能切出”.”
如果按照英文句点”.”,进行切分必须写”\.”(两个反斜杠). \转义字符

static关键字(静态)

一旦用了static关键字,那么这个对象将不再属于对象自己,而是属于所在的类.多个对象共享同一份数据.
静态方法:静态方法不属于对象,而是属于类的.
如果没有static关键字,(方法就就成了成员方法)那么必须首先创建对象,然后通过对象才能使用它.
如果有了static关键字,那么不需要创建对象,直接就能通过类名称来使用它.

public static 数据类型  方法名 (){
方法体;
}

两种调用方法:
A,类名称.方法名(); //在本类中可以不加类名称来使用,编译器会自动补全.
B,对象名.方法名(); //不推介使用. 若使用了此方法,编译器也会自动转换为前一种方法
//对于静态方法来说,可以通过对象名进行调用,也可以通过类名称来调用.
//五论是成员方法还是变量,一旦添加了static,都推介使用类名称来进行调用
注意:静态不能直接访问非静态. /静态内容随类的加载而加载/,成员变量加载在后所以不能调用
原因:因为在内存中是[先]有的静态内容,[后]有的非静态内容.
//静态方法当中不能同this.
原因:this代表当前的对象,通过谁调用的方法,谁就是当前对象.
注意:根据类名称访问静态成员变量的时候,全程和对象就没关系只和类有关系.
//静态方法在内存区中是位于方法区的,不是和对象一样new于堆中的.

静态代码块; //static:随类的创建而创建,随类的消失而消失
格式:

public  class   类名称{
static{
//静态代码块的内容
}}  

//特点:当第一次用到本类时,静态代码块执行唯一的一次
//静态内容总是优先于非静态,所以静态代码块比构造方法先执行.
静态代码块的典型用途.用来一次性地对静态成员变量进行赋值.

Java.util.arrays是一个与数组相关的工具类,里面提供了大量静态方法,用来实现数组常见的操作
public static StringtoString(数组): //将参数数组变成字符串(按照默认格式:[元素1,元素2,元素3,,,,,])
//参考代码:

int intArray= {10,12,12,24};
StringintStr = arrArrays.toString(intString);
System.out.println(intStr);      //输出[10,12,12,24]

public staict void sort(数组):按照默认升序(由小到大)进行排序;
//参考代码:

char[] chars = str.toCharArray();   //将str转换为字符数组.
Arrays.sort(chars)     //对数组进行升序排列

备注:
1如果是数值sort默认按照升序从小到大
2如果是字符串,sort默认按照字母升序
3如果是自定义的类型,那么这个自定义的类需要有Conparable或者Comparator接口的支持.

数学工具

和数学相关的工具类:java.util.math
A<public static double abs(double num); 获取绝对值
B<public static double ceil(double num); 向上取整
C<public static double floor(double num); 向下取整
D<public static double round(double num); 四舍五入 //将小数转换成整数
math.PI 代表近似的圆周率常量
//上面的类型结果都是double结果

继承

面向对象的三大特征:封装\继承\多态
继承是多态的前提,如果没有继承就没有多态.
继承主要解决的是!共性抽取!
//父类 也叫基类\超类
//子类 也可以叫派生类

继承的特点:
A!子类可以拥有父类”内容”
B!子类还可以拥有自己专有的内容

在继承的关系中,”子类就是一个父类”,也就是说子类可以被当作父类看待
例如父类是员工,子类是讲师,那么”讲师就是一个员工”.关系:is-a
定义父类的格式:(一个普通的类定义)

public class  父类名称{

}

定义子类的格式:

public class 子类名称  extends  父类名称{

}

//通过继承实现代码的复用
在父子类的继承关系当中,如果成员变量重名,则创建子类对象时,访问有两种方式;
直接通过子类对象访问成员变量:
//等号左边是谁,就优先用谁,没有则向上找. “=nwe” 的左边
间接通过成员方法访问成员变量:
//该方法属于谁(谁定义了),就优先用谁

如何区分三种变量的重名问题;
A《父类的成员变量; //super.成员变量名
B《本类的成员变量; //this.成员变量名
C《局部的成员变量; //直接写成员变量名

在父子类的继承关系当中,创建子类对象,访问成员方法的规则。
创建的(new)对象是谁,就优先用谁,如果没有则向上找。
注意事项:无论是成员方法还是成员变量,如果没有都是向上找父类,绝不会向下找子类。

继承的特点

Java是单继承的:一个类的直接父类是唯一的

举例:

class A{}
class B extends A{}    //正确

class C{}
class D extends A,C{}    //错误

原因:如果继承多个父类,代码将会冲突.例如两个父类同时打印同名类.

Java语言可以多级继承:父类还可以继承父类

举例:

class A{}
class B extends A{}    //正确
class C extends B{}    //正确

java.lang.Object 类是最高的父类\祖宗类

一个父类的直接父类是唯一的,但一个父类可以有多个子类

参考代码:

class A{}
class B extends A{}    //正确
class C extends A{}    //正确

抽象类

抽象的概念

抽象方法:如果父类当中的方法不确定如何进行{}方法体实现,那么这就是一个抽象方法.

父类的是抽象的方法,子类是具体的方法.

子类的继承是抽象对象的具体化

子类就是一个父类,所以是继承关系.

抽象方法的概念

抽象方法:就是加上abstract关键字,然后去掉大括号,直接分号结束.

抽象类:抽象方法所在的类,必须是抽象类才行.在class之前写上abstract即可

参考代码:

public abstractclass Animal{ //这是一个抽象的类
    public abstractvoid eat();//这是一个抽象方法,但是具体的内容不确定,所以是抽象方法
    public void nullmalMethod(){//这是普通的成员方法
    }
}

抽象类和抽象方法的使用

步骤 抽象方法必须覆盖重写(父债子偿)

  • 1.不能直接创建new抽象类对象
  • 2.必须用一个子类来继承父类
  • 3.子类必须覆盖重写抽象父类当中的所有的抽象方法
  • 覆盖重写(实现):子类去掉抽象方法的abstract关键字,然后补上方法体大括号

  • 4.创建子类对象进行使用

参考代码

父类:

public abstractclass Animal{
public abstractvoid eat();
}

子类

public class Cat extends Animal{
@Override
public  void eat(){     //覆盖父类的eat方法
    System.out.println("猫吃鱼");
}
}

mian类

public static void main(String[] args){
Cat cat = new Cat(); //创建cat对象
cat.eat();    //调用子类的eat方法
}

注意事项:

1.抽象类不能直接创建对象 ,如果创建,编译无法通过而报错.只能创建其非抽象子类的对象

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象的方法没有具体的方法体,没有意义

2.抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的. (成员,包括成员方法和成员变量?)

理解:子类的构造方法中,有默认的super(),需要访问父类构造方法.

抽象方法必须覆盖,但父类的普通方法可以被子类继承.子类有默认的super()方法来访问父类的构造方法.

3.抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类.

理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计

举例:

public abstractclass My{
}

4.抽象的子类,必须重写抽象父类中的所有的抽象方法,否则编译无法通过而报错.除非该子类也是抽象类,

理解:假设不重写所有的抽象方法,则类中可能包含抽象方法.那么创建对象后,调用抽象的方法,没有意义.

继承的案例

案例:群主发红包

image-20201213133420639

案例分析:

用户:群主和成员的共性 (父类)

参考代码(java bin)

public class User{
    private String name;   //姓名
    private int   money;     //余额,也就是当前用户拥有的钱数
    public User(){ 
    }
    public User(Stringname,int money){
        this.name = neme;
        this.money = money;
    }
    //展示一下当前用户有多少钱
    public void show(){
         System.out.println("我是"+name+"余额"+money);
    
    public StringgetName(){
        return name;
    }
    public void setName(Stringnamen){
        this.name = name;
    }
    public int getMoney(){
        return money;
    }
    public void setMoney(int money){
        this.money = money;
    }
    
}

群主java代码


成员java代码


Main方法代码


泛型

【泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java语言引入泛型的好处是安全简单。

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。

对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率。

T、E、K、V、? 这些全都属于java泛型的通配符

多态

继承性是多态性的前提;

extends继承或者implements实现,是多态的前提

举例:小明是一个学生,但同时也是一个人.

一个对象拥有多种形态,这就是对象的多态性

多态的格式

代码当中体现多态性,其实就是:父类引用指向子类对象.

格式:

接口名称 对象名 = new 实现类名称();

或者

父类 对象名 = new子类名称();

public static void main(String[] args){
//使用多态的写法
//左侧父类的引用,指向右侧子类的对象
    Fu ojb = new zi();
    ojb.method();    //使用子类的方法
    ojb.methodF();    //使用父类的方法  在子类中找不到将往上在父类中找
}

多态中使用成员变量的特点

访问成员方法的两种方式

1.通过对象名称访问 (看等号左边是谁,优先用谁 Fu ojb = new Zi(); ) (只有方法能够重写,成员变量不能)

2.间接通过成员方法访问成员变量 (看该方法属于谁优先用谁,没有则向上找 )

注意:如果子类将父类的此方法重写,再使用将用子类的

多态中使用成员方法的特点

在多态的代码中,成员方法的访问规则是:

看new的是谁就优先用谁.没有则向上找

注意:

成员变量:编译看左边,运行看右边

成员方法:编译看左边,运行看右边. 多态只能编译父类的方法,运行在子类

多态的使用

使用多态的好处

如果不用多态,只用子类,那么写法是:
Teacher  one =  new Teacher();
one.work;  //讲课
Assistant two = new Assistant;
two.work;   //辅导

如果使用多态的写法.对比一下
Employee one = new Teacher;
one.work;
Employee two = new Assistant;
two.work;
好处:无论右边new的时候换成那个子类对象,等号左边调用方法都不会变化;

对象的向上转型

对象的向上转型,其实就算多态写法:
格式: 父类名称 对象名 = new 子类名称();            Animal anima = new Cat();
含义:右侧创建一个子类对象,把他当做父类来看待使用.      创建了一只猫,当作动物看待,没问题
注意:向上转型一定是安全的.
//对象的向上转型,就是.父类引用指向子类对象
类似于:
double num = 100;  //正确, int-->double,自动类型转换

向上转型一定是安全的,没有问题的,正确的,但是也有一个弊端;

对象一旦向上转型为父类,那么就无法调用子类原本特有的内容.

对象的向下转型

对象的向下转型,其实是一个[还原]的动作.
格式:  子类名称 对象名 = (子类名称) 父类对象;
含义: 将父类对象,[还原]成为本来的子类对象
Animal animal = new Cat();  //本来是猫,向上转型为动物
Cat cat = (Cat) animal;     //本来是猫,以及被当做了动物,还原回来成为本来的猫
注意:
//必须保证对象本来创建的时候,就是猫,才能向下转型成猫;
//如果对象创建的时候本来不是猫,现在非要向下转型成为猫就会报错

注解和反射

什么是注解:

  • Annotation是从JDK5.0开始引入的新技术).
  • Annotation的作用:
    • 不是程序本身可以对程序作出解释.(这一点和注释(comment)没什么区别)
    • 可以被其他程序(比如:编译器等)读取.
  • Annotation的格式:
    • 注解是以”@注释名”在代码中存在的,还可以添加一些参数值,例如:@SuppressWarnings(value=”unchecked”)
  • Annotation在哪里使用?
    • 可以附加在package , class , method , field等上面﹐相当于给他们添加了额外的辅助信息,我们可以通过反射机制编程实现对这些元数据的访问

内置注解

  • @Overfide∶定义在java.lang.Override 中,此注释只适用于修辞方法.
    • 表示一个方法声明打算重写超类中的另一个方法声明.
  • @Deprecated:定义在java.lang.Deprecated中,此注释可以用于修辞方法,属性,类,
    • 表示不鼓励程序员使用这样的元素,通常是因为它很危险或者存在更好的选择.
  • @SuppressWarnings:定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息
    • 与前两个注释有所不同,你需要添加一个参数才能正确使用,这些参数都是已经定义好了的,我们选择性的使用就好了.
    • @SuppressWarnings(“all”)
    • @SuppressWarnings(“unchecked”)
    • @SuppressWarnings(value={“unchecked”,”deprecation”})

元注解

元注解的作用就是负责注解其他注解) , Java定义了4个标准的meta-annotation类型,他们被用来提供对其他annotation类型作说明

这些类型和它们所支持的类在java.lang.annotation包中可以找到 (@Target , @Retention ,@Documented , @lnherited )

@Target

此注解说明注解的作用目标 (即:被描述的注解可以用在什么地方),默认值为任何元素

  • TYPE——接口(包括Annotation,即@interface)、类、枚举声明]
  • FIELD——字段(包括枚举常量)声明,@CFNotNull
  • METHOD——方法声明,如@Autowired
  • PARAMETER——参数声明,如@Param
  • CONSTRUCTOR—— 构造函数什么,如@ConstructorProperties
  • LOCAL_VARIABLE——局部变量声明,如@CFNotNull
  • ANNOTATION_TYPE——注释类型声明,如@Target
  • PACKAGE——包声明
  • TYPE_PARAMETER——类型参数声明
  • TYPE_USE——类型的使用

@Retention:

表示定义被它所注解的注解保留多久, 用于描述注解的生命周期

source:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略

class:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期

runtime:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

那怎么来选择合适的注解生命周期呢?

首先要明确生命周期长度: SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。

一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解

如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife,就用 CLASS注解

如果只是做一些检查性的操作,比如 @Override@SuppressWarnings,则可选用 SOURCE 注解

@Document:

表明这个注解应该被 javadoc工具记录,正常情况下javadoc中不包含注解的,@Documented属于标志注解。

@Inherited:

说明子类可以继承父类中的该注解

自定义注解

  • 使用@interface自定义注解时﹐自动继承了java.lang.annotation.Annotation接口

  • 分析:

  • interface用来声明一个注解,格式:

    • public @interface 注解名 {定义内容}
      
  • 其中的每一个方法实际上是声明了一个配置参数

  • 方法的名称就是参数的名称.

  • 返回值类型就是参数的类型(返回值只能是基本类型,Class , String , enum )

  • 可以通过default来声明参数的默认值

  • 如果只有一个参数成员,一般参数名为value

  • 注解元素必须要有值,我们定义注解元素时,经常使用空字符串,0作为默认值.

格式:

public class Shaun {
    @注解名称
    void show();
}

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface 注解名称{   //@interface + 注解名
    //参数类型+ 参数名()  [default 默认值]
    String name() default "null";
    int number() default 0;   //如果为-1,代码不存在
    String[] array() default{"1","注解","注释"};
}
//注解没有顺序

反射

静态VS 动态语言

动态语言
是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
主要动态语言: Object-C、C#、Javasbript、 PHP、Python等。
静态语言
与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。

Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性
Java的动态性让编程的时候更加灵活!

Java Reflection

Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

Class c = Class.forName("java.lang.String")

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射

正常方式:引入需要的”包类”名称 通过new实例化 取得实例化对象
反射方式:实例化对象 getClass()方法 得到完整的“包类”名称

Java反射机制研究及应用

Java反射机制提供的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

Java反射优点和缺点

优点:
可以实现动态创建对象和编译,体现出很大的灵活性

缺点:
对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于直接执行相同的操作。

反射相关的主要API:

java.lang.Class:代表一个类

java.lang.reflect.Method:代表类的方法

java.lang.reflect.Field :代表类的成员变量

java.lang.reflect.Constructor:代表类的构造器

  • 一个类在内存中只有一个class对象

    • 举例

    • int[] a = new int[10];
      int[] b = new int[100];
      System.out.println(a.getclass().hashCode());
      System.out.println(b.getclass().hashcode());
      
    • 它们是个同一个Class

  • 一个类被加载后, 类的整个结构都会被封装在Class对象中

反射和new 出来的对象并无本质差别,但是反射有一个好处就是,程序运行起来了也能够不改动原来的代码的情况下,只需要加新的代码,加入新功能,大大降低耦合度

Class类

Object类中定义了以下的方法,此方法将被所有子类继承

public final Class getClass()

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。一个Class对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/)的有关信息。

  • Class本身也是一个类
  • Class对象只能由系统建立对象
  • 一个加载的类在JVM中只会有一个Class实例一个Class对象对应的是一个加载到 JVM 中的一个.class文件
  • 每个类的实例都会记得自己是由哪个Class 实例所生成>通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

常用方法

方法名 功能说明

获取Class类的实例

a)若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高。

Class clazz = Person.class;

b)已知某个类的实例,调用该实例的getClass()方法获取Class对象

Class clazz = person.getClass();

C)已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException

Class clazz = Class.forName("demo01.Student");

d)内置基本数据类型可以直接用类名.Type

e)还可以利用ClassLoader

Person person = new student();
System.out.println("这个人是:"+person. name);

//方式一︰通过对象获得
class c1 = person.getclass();
system.out.println( c1.hashcode());

//方式二 : forname获得
class c2 = class.forName( "com.kuang.reflection.Student");
system.out.println( c2.hashcode());

//方式三:通过类名.class获得
class c3 = Student.class;
system. out.println(c3.hashcode());

//方式四:基本内置类型的包装类都有一个Type属性
class c4 = Integer.TYPE;
system.out.println(c4);

//获得父类类型
class c5 = c1.getsuperclass();
system.out.println(c5);

哪些类型可以有Class对象?

  • class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类。interface:接口
  • []:数组
  • enum:枚举
  • annotation:注解@interface
  • primitive type:基本数据类型
  • void
//所有类型的class
public class Test04 {
    public static void main( String[] args) {
        Class cl = Object.class;		//类
        Class c2 = Comparable.class;	//接口
        Class c3 = String[].class;		//一维数组
        Class c4 = int[][].class; 		//二维数组
        Class c5 = Override.class;		//注解
        Class c6 = ElementType.class;	//枚举
        Class c7 = Integer.class; 		//基本数据类型
        Class c8 = void.class; 			//void
        Class c9 = Class.class;			//class
    }
}

类的加载与ClassLoader的理解

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

  1. 类的加载(Load): 将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
  2. 类的链接(Link): 将类的二进制数据合并到 JRE 中
  3. 类的初始化(Initialize): JVM 负责对类进行初始化
  • 加载:
    • 将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象.
  • 链接: 将Java类的二进制代码合并到JVM的运行状态之中的过程
    • 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
  • 初始化:
    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

static优先级高,先执行,同为static的根据从上到下顺序执行

Class对象在类加载时就会产生,此时默认初始化值为0或者null,只有在调用或者实例化时才会执行静态代码块

什么时候会发生类初始化?

  • 类的主动引用(一定会发生类的初始化)
    • 当虚拟机启动,先初始化main方法所在的类
    • new一个类的对象
    • 调用类的静态成员(除了final常量)和静态方法
    • 使用java.lang.reflect包的方法对类进行反射调用
    • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
  • 类的被动引用(不会发生类的初始化)
    • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
    • 通过数组定义类引用,不会触发此类的初始化
    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

类加载器的作用

类加载器作用是用来把类(class)装载进内存的。JVM规范定义了如下类型的类的加载器。

  • 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取
  • 扩展类加载器:负责jre/lib/ext目录下的jar包或-D java.ext.dirs 指定目录下的jar包装入工作库
  • 系统类加载器:负责java-classpath 或java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器

创建运行事来类的对象

通过反射获取运行时类的完整结构
Field、Method.Constructor、Superclass、InterfaceAnnotation

  • 实现的全部接口
  • 所继承的父类
  • 全部的构造器
  • 全部的方法
  • 全部的Field
  • 注解
  • ……

获得类的运行时结构

class c1 = class.forName ( "com.kuang.reflection.User") ;
//获得类的名字
system.out.println(c1.getName()); 			//获得包名+类名
system.out.println(c1.getSimpleName());		//获得类名

获得类的属性

//获得类的属性
system.out.println("=======================");
Field[] fields = c1.getFields();//只能找到public属性
fields = c1.getDeclaredFields(); //找到全部的属性
for (Field field : fields) {
	system.out.println( field) ;
}
//获得类的属性
system.out.println("=======================");
Field[] fields = c1.getFields();			//只能找到public属性
fields = cl.getDeclaredFields();			//找到全部的属性
for (Field field : fields) {
	system.out.println(field);
}
//获得指定属性的值
Field name = c1.getDeclaredField("name" );
System.out.println(name) ; 

获得类的方法

//获得类的方法
system.out.println("=========================");
Method[] methods = c1.getMethods();			//获得本类及其父类的全部public方法
for (Method method : methods) {
	system.out.println("正常的:"+method ) ;
}
methods = c1.getDeclaredMethods();			//获得本类的所有方法
for (Method method : methods)i
	system.out.println( "getDeclaredMethods: "+method);
)

获得构造器

//获得指定方法 //重载
Method getName = c1.getMethod( name: "getName",null)
Nethod setName = c1.getMethod( name: "setName",String.class);
system.out.println(getName);
system.out.println(setName);
//获得指定的构造器
system.out.println("=====================");
constructor[] constructors = c1.getConstructors();
for (constructor constructor : constructors) {
	system.out.println( constructor ) ;
}
constructors = c1.getDeclaredConstructors();
for (constructor constructor : constructors) {
	system.out.println("#"+constructor) ;
}
//获得指定的构造器
Constructor declaredConstructor = c1.getDeclaredConstructor(String.class,int.class,int.class);
system.out.println("指定: "+declaredConstructor);

所以安全的单例 需要使用枚举,枚举不允许反射获取对象,反序列化得到的也是同一个对象

有了Class对象,能做什么?

  • 创建类的对象:调用Class对象的newlnstance()方法
    1. 类必须有一个无参数的构造器。
    2. 类的构造器的访问权限需要足够

思考?难道没有无参的构造器就不能创建对象了吗?只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。

  • 步骤如下:
    1. 通过Class类的getDeclaredConstructor(Class …parameterTypes)取得本类的指定形参类型的构造器
    2. 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
    3. 通过Constructor实例化对象

调用指定的方法

通过反射,调用类中的方法,通过Method类完成。

  1. 通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型。
  2. 之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象的参数信息。

Object invoke(Object obj, Object … ar:gs)

  • Object对应原方法的返回值,若原方法无返回值,此时返回null
  • 若原方法若为静态方法,此时形参Object obj可为null
  • 若原方法形参列表为空,则Object[] args为null
  • 若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的 setAccessible(true) 方法,将可访问private的方法。

setAccessible()

  • Method和Field、Constructor对象都有setAccessible()方法。
  • setAccessible作用是启动和禁用访问安全检查的开关
  • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
    • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
    • 使得原本无法访问的私有成员也可以访问
  • 参数值为false则指示反射的对象应该实施Java语言访问检查

accessible: 可利用,可访问

反射操作泛型

  • Java采用泛型擦除的机制来引入泛型,Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换问题,但是,一旦编译完成,所有和泛型有关的类型全部擦除
  • 为了通过反射操作这些类型, Java新增了ParameterizedType,GenericArrayType ,TypeVariable和WildcardType几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型.
  • ParameterizedType:表示一种参数化类型,比如Collection
    GenericArrayType:表示一种元素类型是参数化类型或者类型变量的数组类型
  • TypeVariable :是各种类型变量的公共父接口
  • WildcardType:代表一种通配符类型表达式

利用反射操作注解

框架的实现都是在反射和注解的基础上的。

  • getAnnotations 获得注解
  • getAnnotation 获得注解的值

了解什么是ORM ?

  • Object relationship Mapping –>对象关系映射

    • 类和表结构对应

    • 属性和字段对应

    • 对象和记录对应

  • 要求:利用注解和反射完成类和表结构的映射关系

举例:

自定义注解

//类名的注解
@Target(ElementType. TYPE)
@etention(RetentionPolicy.RUNTIME)
@interface Tablekuang{
string value();
}
//属性的注解
@Target(ElementType.FIELD)
@etention(RetentionPolicy. RUNTIME)
@interface Fieldkuang{
	String columnName();
    String ype();
	int length();
}

注解的使用

@Tablekuang ("db_student")
class stufdent2{
	@Fieldkuang(columnName = "db_id",type = "int" , length = 10)
    private int id;
	@Fieldkuang(columnName = "db_age" ,type = "int", length = 10)
    private int age;
	@Fieldkuang(columnName = "db_name" ,type = "varchar" ,length = 3)
    private String name;
    
	public student2() {}
	public student2 (int id,int age,string name) {
        this.id = id;
        this.age = age;this.name = name;
}

通过反射获取注解的信息

//通过反射获得注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations) {
	System.out.println( annotation);
}

//获得注解的value的值。
Tablekuang tablekuang = (Tablekuang)c1.getAnnotation(Tablekuang.class);
String value = tablekuang.value();
System. out.println(value);

//获得类指定的注解
Field f = c1.getDeclaredField("id");
Fieldkuang annotation = f.getAnnotation(Fieldkuang.class);
System.out.println(annotation.columnName());
System.out.println(annotation.type()));
System.out.println(annotation.length());

output:
	com.kuang.reflection.Tablekuang(value=db_student)
    db_student
	db_id
	int10
版权声明:本文为xuanstudy原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xuanstudy/p/16576699.html