JAVA学习笔记

    科技2022-07-13  126

    JAVA学习笔记

    package的使用

    类似C++之中的namespace. package表示该程序所属的包,它只能有一个或者没有。如果有,必须放在最前面;如果没有,那该程序属于默认包。默认包视运行目录而定 一般在建立项目后,再建立包,再建立类,应用会自动声明当前类所在的包。

    声明这个类所属的包,方便区分同名变量,同时方便其它包的程序调用这个类定义的方法

    package的命名

    一直使用默认包会导致冲突,JVM之中只允许同一个

    Java建议采用Internet域名的倒序作为包的前缀。

    通常包名都用小写字母命名.

    以百度公司为例,域名为 www.baidu.com

    则在获得公司许可前提下,可使用以下包名

    com.baidu.www.mypackage

    在该包新建一个Mian类,它的所在位置如下

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zBtjGhWA-1610026071603)(C:\Users\Platina\AppData\Roaming\Typora\typora-user-images\image-20210103182239907.png)]

    可以看到,包名不仅仅是简单的包名,并且包名与文件系统的目录结构一一对应.

    可以看到每个点都是一个一级目录的分隔点

    Java中的每个类都属于某一个包, 类在编译时被添加到包中. 默认情况下, 类在编译时放在当前目录(默认包).

    把类添加到指定包中, 只需在源程序的最前端加上:

    package packageName;

    库的引入

    import java.io.*

    与C语言中的 #include " " 作用类似

    除了package和import语句以外,其它执行操作的语句,都只能在类的大括号里面

    如果要使用其他包之中的类,要使用import引入其它包

    源程序代码文件基本结构

    package package01; // 包语句 import java.util.Scanner; // 导入语句(可选) public class FirstApplication { // 定义类、接口 public static void main(String[] args) { // 主方法,程序的起点 Scanner input = new Scanner(System.in); System.out.print("Input 2 intgers: "); int a = input.nextInt(); int b = input.nextInt(); int c = a + b; System.out.println(a + "+" + b + "=" + c); } }

    注意事项

    Java允许在一个源程序代码文件里面编写多个类,但强烈建议 :一个源程序代码文件里面只编写一个类,并用 public 修饰同一个源程序代码文件最多只有一个类用public 修饰。并且要求源程序代码文件名与public类的类名相同(包括字母大小写)编译源程序(Windows下,使用javac),每个类生成一个拓展名为class的字节文件可以分别编译每个类,也可以直接编译主类,编译器会自动先编译需要的类JRE Java Runtime Environment java运行环境

    数据类型

    增加的类型:byte

    范围 :-128——127;类型:整形

    字符型

    字节大小:2 字节取值:0-65535编码集:Unicode编码

    Unicode编码和ASCII码

    Unicode前128位刚刚好是ASCII表

    都可以让字符数据转换成整数

    区别 整数代表Unicode表中的编码,必须进行强制类型转换

    int c = 97; char s = (char)c;

    ASCII码可以直接转换

    int n = 97; char a = n; // a 表示字母 'a'

    数值型常量的用法

    1.引入对应的类:import java.lang.数据类型 由于java.lang使用较为频繁,已默认加载,现无需引入

    2.使用: 最小值: 数据类型.MIN_VALUE 最大值: 数据类型.MAX_VALUE

    3.其中数据类型可为:Byte、Short、Integer、Long、Float、Double

    常量的声明

    C++

    const double PI 3.14

    define 和 const 的区别

    define是宏定义,程序在预处理阶段将用define定义的内容进行了替换。因此程序运时,常量表中并没有用define定义的常量,系统不为它分配内存。 const定义的常量,在程序运行时在常量表中,系统为它分配内存。

    define定义的常量,预处理时只是直接进行了替换。所以编译时不能进行数据类型检验。 const定义的常量,在编译时进行严格的类型检验,可以避免出错。

    define定义表达式时要注意“边缘效应”,例如如下定义:

    #define N 2+3 //我们预想的N值是5,我们这样使用N int a = N/2; //我们预想的a的值是2.5,可实际上a的值是3.5

    原因是 N 用2+3替代,则 a = 2+3/2 = 3.5 故应避免用宏定义含有运算符的式子

    所以通常不把define宏定义的量称为常量

    JAVA

    final float PI = 3.14;

    区别:JAVA需要声明常量类型以及需要等号和分号 注意:JAVA的声明需要在类里面,否则会报错(除了包语句和导入语句以外,其他语句都需要在类里面) 栗子 正确声明 这里再讲一下我遇到的另外一个问题, 错误: 无法从静态上下文中引用非静态 变量 this 由于有网友做了解答我就直接引用了hh 文章链接 主要是 没有将声明实例化 就直接使用,所以会报错 正确用法 先将声明实例化

    Hello A = new Hello( ); size = A.PI*r*r;

    或者将常量PI 变成静态常量也可以解决

    public class test { static final double PI = 3.1415926; public static void main(String[] args){ System.out.println("PI = " + PI); } } // 输出结果:PI = 3.1415926

    运算逻辑

    运算遵循的原则

    严格遵循从左到右;按照运算符的优先级进行运算符优先级相同,则根据运算符结合方向结合

    栗子

    int a = 0; int x1 = a++ + a++ + a++; // 根据从左到右原则,则对应每项为 0、1、2 a = 0; int x2 = ++a + ++a + ++a; // 根据从左到右原则,则对应每项为 1、2、3 System.out.println(x1 + " " + x2); // 运行结果为 3 6

    C语言没有这种严格从左到右的原则,在不同编译器编译上述代码,可能会出现不同结果

    数据类型转换

    不同数据类型混合进行运算,根据数据类型大小进行自动转换,再进行运算

    byte < short < char < int < long < float < double

    需要注意:整形数据类型运算最低级别为 int 类型

    不同类型数据算术运算时的转换规则:

    1.如果运算对象之一是double,就将另一个转换为double型。

    2.否则,如果运算对象之一是float,就将另一个转换为float型。

    3.否则,如果运算对象之一是long,就将另一个转换为long型。

    4.否则,两个运算对象都转换为int型。(int 是Java 运算的最小类型)

    拓展赋值运算符

    和 C 语言之中差别不大,主要讲一个区别,赋值运算符会自动进行强制类型转换

    short s1 = 1; s1 = s1 + 1; //会报错 short s2 = 1; s2 += 1; // ==> s2 = (short)(s2 + 1);

    这个读者可以自己编译看看

    流程控制

    对于多个判断条件,最好先写好各个判断的条件,再开始写具体的语句

    switch 语句

    和 C 语言的区别是判断的表达式可以是 String

    栗子

    String s; Scanner input = new Scanner(System.in); s = inuput.next(); switch(s) { case "张三" : System.out.println("张三获胜"); break; case "李四" : System.out.println("李四获胜"); break; default: System.out.println("Error"); }

    break 和 continue 的新用法

    标号的使用

    outer: for(int i = 0; i < 10 ;i++) { for(int j = 0; j < 10 ;j++) { if(i == 3) continue outer; // 跳到标号 outer 指代的循环的 i++ ,再进入判断 i<10 a[i][j] = i; if(i>5) break outer; // 跳出标号 outer 指代的循环 } }

    标号的使用可以快速跳出多重循环,提高算法效率

    String类的使用方法

    C++用法

    https://blog.csdn.net/manonghouyiming/article/details/79827040

    主要讲一个区别,java之中没有~~s[i]~~这种用法

    Scanner input = new Scanner(System.in); String s = input.nextLine(); System.out.println(s[0]); //会报错

    java中得到第几个元素需要使用 charAt()方法

    Scanner input = new Scanner(System.in); String s = input.nextLine(); System.out.println(s.charAt(0)); // 输入 test // 输出 t

    String类型常用方法

    常用方法功能说明public int length()返回字符串的长度public boolean equals(Object anObject)将给定字符串与当前字符串相比较,若两字符串相等,则返回true,否则 返回falsepublic String substring(int beginIndex)返回字符串中从beginIndex开始到字符串末尾的子串public String substring(int beginIndex,int endlndex)返回从beginIndex开始到endIndex-1的子串public char charAt(int index)返回index指定位置的字符public int indexOf(String str)返回str在字符串中第一次出现的位置public int compareTo(String anotherString)若调用该方法的字符串大于参数字符串,则返回大 于0的值,若相等则返回0,小于参数字符串则返回小于0的值public String replace(char oldChar, char newChar)以newChar字符替换字符串中所有oldChar字符public String trim()去掉字符串的首尾空格public String toLowerCase()将字符串中的所有字符都转换为小写字符public String toUpperCase()将字符串中的所有字符都转换为大写字符

    格式化方法:String.(String format, Object… args)

    参数格式同printf();

    键盘输入数据

    书上有两种方法,这里仅介绍第二种

    步骤

    1.引用对应的类库

    import java.util.Scanner //包含专门用于输入的类 Scanner

    2.使用Scanner类创建对象

    Scanner reader = new Scanner(System.in); //创建 Scanner对象用于读取System.in的输入

    3.声明变量类型

    int a;

    4.调用 reader 对象的对应方法,读取键盘数据

    a = reader.nextInt();

    对应的方法包括:nextByte()、nextDouble()、nextFloat()、nextInt()、nextLong()、nextShort()、next()、nextLine() 形式:next数据类型() 用法:对应数据类型代表需要从键盘获取的数据的类型

    next()和nextLine()的异同

    相同: 都是用于从键盘读取字符串的

    不同: next()方法一定要读取到有效字符后才可以结束输入,对输入有效字符之前遇到的空格键、Tab键或Enter 键等,next()方法会自动将其去掉,只有在输入有效字符之后,next()方法才将其后输入的空格键、Tab键或Enter键视为分隔符或结束符;而nextLine()方法的结束符只是Enter键,即nextLine()方法返回的是Enter键之前的所有字符

    Scanner类的对象使用完毕后不能关闭。 不然 整个 System.in 会被关闭,可能严重影响其它资源运行

    数组

    java.util.Arrays

    Java 提供了一个数组相关的工具包,集合了数组的常用方法,读者可以自行查看API 文档

    声明方式

    一维数组

    C语言

    int a[10];

    JAVA 需要先声明,再申请空间才能使用

    int[] a = new int[10];

    第一个[]声明是数组类型,相当于只有一个数组名字 第二个[10]里面数字代表申请空间个数,int[10]代表申请10个存放int类型数据的空间 new 用于申请空间

    特殊初始化

    int[] a = {1,2,3,4,5};

    像这样声明了一个整形数组a,虽然没有指定数组长度,但由于括号里面初值有5个,所以编译器会按照次序存放在a[0]-a[4].

    多维数组

    Java语言没有真正的多维数组。

    所谓多维数组,是数组元素也是数组的数组。

    声明数组

    int[][] a = new int[10][10] 二维数组 以此类推,三维就是三个框框

    // 方式1:同时创建整个多维数组对象 int[][] x = new int[3][4]; //同时指定行、列数 // 方式2:分别创建 “行对象”和 “列对象” int[][] matrix = new int[5][]; //创建 “行对象” matrix[0] = new int[] { 1, 2, 3, 4, 5 }; //创建 “列对象” matrix[1] = new int[] { 2, 3, 4, 5 }; matrix[2] = new int[] { 3, 4, 5 }; matrix[3] = new int[] { 4, 5 }; matrix[4] = new int[] { 5 }; // 多维数组每一行都是一个数组,都有length属性,可以输出每行的长度 x.length = 3 // 行数 x[0].length = 4 // 列数 x[1].length = 4 x[2].length = 4 // 第二个同理

    新的遍历方式

    for( type element : array ) { System.out.println(element); ··· }

    栗子

    int[] a = {12345}for( int element : a) System.out.println(element);

    类类型数组

    顾名思义,就是数组元素是类的数组 用数组来存放对象,一般要经过如下两个步骤:

    (1)声明类类型的数组变量,并用new运算符分配内存空间给数组;

    (2)用new创建新的对象,分配内存空间给它,并让数组元素指向它。 先看例子

    class Spot { double x; double y; } Spot[] a = new Spot[n + 5]; // 类类型数组的声明和普通数组声明类似

    需要注意的是:类类型的数组使用new 进行初始化,仅仅是获得了对应的可以存放对象的地址的空间,所以在上述讲的是,“让数组元素指向它”,其中的”它“就是使用 new 创建的新的对象

    这里可以类比C 语言中的结构体数组

    struct student { char name[10]; char sex[5]; }; struct student a[10+5];

    定义过程比较类似,但初始化方法并不相同 先讲C 语言中的,直接 cin>>a[0].name 即可 下面是建立一个Spot类数组,然后给每个数组元素赋值的操作

    import java.util.Scanner; public class test{ public static void main(String[] args){ System.out.print("输入二维坐标系中点的个数: "); Scanner input = new Scanner(System.in); int n = input.nextInt(); Spot[] a = new Spot[n + 5]; // a 获得了 n+5 个 “可以存放Spot 类对象地址” 的空间 System.out.printf("输入%d个点的横坐标和纵坐标:\n", n); for (int i = 0; i < n; i++) { Spot temp = new Spot(); // 每次需要重新申请空间,因为之前的空间已经使用了 temp.x = input.nextDouble(); temp.y = input.nextDouble(); a[i] = temp; // 让数组元素指向它 } for (int i = 0; i < n; i++) { System.out.println(a[i].x + " " + a[i].y); // 打印Spot类数组元素,确保初始化成功 } } } class Spot { double x; double y; }

    Java的输出语句

    System.out.print( ); System.out.println( );

    以上两个语句类似C++ 之中的cout<< println( )较print( )多了一个默认换行 具体使用方法见下栗子

    int a = 1, b = 2; System.out.println(a + " + " + b + " = " + (a+b)); System.out.print(a + " + " + b + " = " + a+b);

    读者需要注意的是上述语句之中,(a+b) 和 a + b 在 含有""的输出语句之中的差别,具体看下图

    源代码如下

    public class test{ public static void main(String[] args){ int a = 1, b = 2; System.out.println(a + " + " + b + " = " + (a + b)); System.out.print(a + " + " + b + " = " + a + b); } }

    运行结果如下

    可以看到 (a+b) 代表的是 a 和 b 的和,而 a+b 仅仅代表输出 a 后接着输出 b.

    需要注意,以上区别仅仅在存在"“内容条件下成立,若输出语句之中不含”",则本结论不成立

    第三种和C语言中的printf比较类似,相关用法几乎完全相同 System.out.printf ( ); 栗子

    int a = 1, b = 2; System.out.printf("a + b = %d\n",a+b);

    需要注意的是,double 型变量在 C 中的是 %lf ,而java 中是 %f

    面向对象

    数据域封装

    什么时候都需要封装

    使用 private 关键字去封装数据

    根据变量使用范围确定是否提供访问器get…( )方法(bollean类型变量为is…( ))以及修改器set…( )方法

    变量使用要求提供方法non-read-write不可读写不提供get…( ) 和 set…( )readOnly只读只提供get…( )writeOnly只写只提供set…( )read-write读写提供get…( )和set…( ) public class Book { private String isbn; private boolean notPublished; public String getIsbn() { return isbn; } public void setIsbn(String isbn) { this.isbn = isbn; } public boolean isNotPublished() { return notPublished; } public void setNotPublished(boolean notPublished) { this.notPublished = notPublished; } }

    this 关键字的使用

    在类的构造方法中使用,调用本类的其他构造方法。 语法:this(实参表) 如果使用该语句,必须在构造方法的第一句(显性的第一句)

    在类的构造方法和实例方法中使用,代表对当前对象的引用。 访问实例变量语法:this.实例变量名 方法实例方法语法:this.实例方法名(实参表) 每个构造方法和实例方法中都隐含有一个this引用。

    public class Rectangle { double width; double height; public Rectangle(double width, double height) { this.width = width; //this必须使用,引用正在创建的对象 this.height = height; } public Rectangle() { this(1.0, 1.0); //调用本类的其他构造方法 } public void setWidth(double width) { this.width = width; //this必须使用,引用当前对象 } public double getArea() { return this.width * this.height; //this可以省略 } }

    初始器的使用

    静态初始化器与实例初始化器

    初始化器是直接在类中定义的用一对{}括起来的语句组。

    静态初始化器使用static关键字修饰,用来初始化静态变量。

    实例初始化器没有修饰关键字,用来初始化实例变量。

    一个类可以有0个或多个静态和实例初始化器。

    静态初始化器的执行:类首次加载到内存时,首先是静态数据域变量的变量初始化;然后按排列顺序执行类中static初始化器。

    实例初始化器的执行:每次使用 “new 构造方法();” 创建对象时,首先是实例数据域变量的变量初始化,然后开始执行本类的构造方法,但在构造方法第一条语句执行之前(隐式第一,比构造方法还前),按排列顺序执行类中的实例初始化器,然后执行构造方法中的剩余语句。

    继承

    子类的构造方法

    对象类型和 instanceof 运算符

    instanceof 运算符

    !!!是一个运算符

    运算符语法:对象 instanceof 类

    运算结果:boolean

    运算规则:如果对象的类是后面的类或其子类,返回true;否则返回false。

    class A{} class B extends A {} class C extends B {} class D extends C {} class E extends C {} public static void main(String[] args){ D d = new D(); System.out.println(d instanceof A); // 结果为:true System.out.println(d instanceof B); // 结果为:true System.out.println(d instanceof C); // 结果为:true System.out.println(d instanceof D); // 结果为:true System.out.println(d instanceof Object); // 结果为:true System.out.println(d instanceof E); // 结果为:false }

    使用instanceof 判断一个对象是否属于某个类只是初略判断。

    精确判断一个对象的类:

    对象.getClass() == 类名.class

    子类转父类

    父类类型的对象引用可以引用一个父类对象或者任何的子类对象. 称为隐式类型转换(向上类型转换)。

    Object obj = new Student(); // System.out.println(obj.getClass()); //结果为: class 包名.Student // 上面等价于下面 Student stud = new Student(); Object obj = stud;

    注意:第一条语句,Object 类型的引用变量obj引用的是他的一个子类 Student 的对象,所以obj.getClass(),得到的结果为: class 包名.Student

    栗子

    class A{} class B extends A {} class C extends B {} class D extends C {} class E extends C {} public static void main(String[] args){ Object d = new D(); // 实际上 Object 类型的引用变量 d 引用的是 D 类的一个对象 System.out.println(d.getClass()); // 结果为:class 包名.D System.out.println(d instanceof A); // 结果为:true System.out.println(d instanceof B); // 结果为:true System.out.println(d instanceof C); // 结果为:true System.out.println(d instanceof D); // 结果为:true System.out.println(d instanceof Object); // 结果为:true System.out.println(d instanceof E); // 结果为:false }

    父类转子类

    如果没有显式地强制类型转换, 一个子类类型的对象引用不可以引用父类对象. 把父类类型强制转换为子类类型称为向下强制类型转换。

    Object obj = new Date(); Date date = obj; // 编译出错 Date date = (Date)obj; // 正确 // 下面语句无语法错误, 但是导致运行错误 Date d = (Date) new Object();

    不能进行平等转换

    如果两个类类型是平等的,即任何一个都不是另一个的祖先或后代,则这两个类类型不能进行类型转换。

    // Date 类型和 String 类型无对应继承继承关系,不能进行类型转换 Date d = new String(); // 语法错误 Date d = (Date) new String(); // 语法错误 String str = (String )new Date(); // 语法错误

    多态性

    方法覆盖

    对象:实例方法

    使用场景:父类和子类中有相同的方法,但是其实现(方法体)不同。

    覆盖规则

    子类定义一个与继承实例方法同名的方法。

    子类方法的返回类型要求:

    基本类型或void:与父类方法的返回类型相同

    类:与父类方法返回类型相同或是返回类型的子类

    子类方法的形参的个数、类型必须与父类方法完全相同。参数名称可以不同。

    访问权限:子类方法的访问权限必须大于等于父类方法的访问权限。

    辅助修饰符 @Override

    用法:加在重写的方法的前一行

    作用:检查该方法重写是否符合语法规范

    方法重写

    对象:静态方法

    作用:隐藏父类的方法,并没有覆盖

    super关键字

    关键字super在子类中使用,主要有2种用途:

    调用父类的构造方法:super( ),super(args0,args1…)

    访问已经继承的成员变量和实例方法

    子类访问继承自父类的实例成员的语法,无论否被隐藏或重写。

    super.方法名(参数)

    super.成员变量

    equals 方法重写

    @Override public boolean equals(Object other) { if (this == other) { return true; } if (other == null) { return false; } if (this.getClass() != other.getClass()) { return false; } // 前三个都一样 // 后面根据要求定义是否相等 Product t = (Product) other; if (!this.id.equals(t.id)) { return false; } if (!this.name.equals(t.name)) { return false; } if (Double.doubleToLongBits(this.price) != Double.doubleToLongBits(t.price)) { return false; } if (this.inventory != t.inventory) { return false; } return this.shelfDate.equals(t.shelfDate); }

    hashCode 方法重写

    注意:equals()方法比较了哪些数据域的变量,重写hashCode()方法就要使用上那些变量

    下面是对应上面的equals()方法的hashCode()的重写

    @Override public int hashCode(){ return Objects.hash(id,name,price,inventory,shelfDate); }

    相关知识连接

    https://www.cnblogs.com/skywang12345/p/3324958.html

    https://www.cnblogs.com/myseries/p/10977868.html

    hashCode散列码计算(来自:Effective Java)

    把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤: 为该域计算int类型的散列码c: 如果该域是boolean类型,则计算(f?1:0)。如果该域是byte,char,short或者int类型,则计算(int)f。如果该域是long类型,则计算(int)(f^(f>>>32))。如果该域是float类型,则计算Float.floatToIntBits(f)。如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.1.3,为得到的long类型值计算散列值。如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式(canonical representation),然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(其他常数也行)。如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.2中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。 按照下面的公式,把步骤2.1中计算得到的散列码c合并到result中:result = 31 * result + c; //此处31是个奇素数,并且有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:31*i == (i<<5) - i, 现代JVM能自动完成此优化。 返回result检验并测试该hashCode实现是否符合通用约定。

    简单的重写

    利用Objects类提供的hash()方法

    @Override public int hashCode() { return Objects.hash(lengthOfSide, numberOfSides, x, y); } public int myHashCode() { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(lengthOfSide); result = prime * result + (int) (temp ^ (temp >>> 32)); result = prime * result + numberOfSides; temp = Double.doubleToLongBits(x); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(y); result = prime * result + (int) (temp ^ (temp >>> 32)); return result; }

    两个代码是等价的,可能计算出的哈希代码不同,则是Object.hash()的参数顺序和求解哈希代码的变量处理顺序不同导致的,只需要将参数顺序和求解哈希代码是变量处理顺序修改成一致即可,如以上代码,计算出的哈希代码相同

    接口

    类似抽象类,但两者各有优缺点

    实现关键字:implements

    特性:可以同时实现多个接口,在一定程度上实现多重继承的效果

    UML类图

    引用来源

    在线链接:http://www.uml.org.cn/oobject/201211231.asp

    本地文件:深入浅出ML类图.html

    类的图示

    图1对应的Java代码片段如下:

    public class Employee { private String name; private int age; private String email; public void modifyInfo() { ...... } }

    在UML类图中,类一般由三部分组成:

    (1) 第一部分是类名:每个类都必须有一个名字,类名是一个字符串。

    (2) 第二部分是类的属性(Attributes):属性是指类的性质,即类的成员变量。一个类可以有任意多个属性,也可以没有属性

    UML 规定属性的表示方式为: 可见性 名称:类型 [ = 缺省值 ]

    其中:

    “可见性”表示该属性对于类外的元素而言是否可见,包括公有(public)、私有(private)和受保护(protected)三种,在类图中分别用符号+、-和#表示。“名称”表示属性名,用一个字符串表示。“类型”表示属性的数据类型,可以是基本数据类型,也可以是用户自定义类型。“缺省值”是一个可选项,即属性的初始值。

    (3) 第三部分是类的操作(Operations):操作是类的任意一个实例对象都可以使用的行为,是类的成员方法。

    UML规定操作的表示方式为: 可见性 名称(参数列表) [ : 返回类型]

    其中:

    “可见性”的定义与属性的可见性定义相同。“名称”即方法名,用一个字符串表示。“参数列表”表示方法的参数,其语法与属性的定义相似,参数个数是任意的,多个参数之间用逗号“,”隔开。“返回类型”是一个可选项,表示方法的返回值类型,依赖于具体的编程语言,可以是基本数据类型,也可以是用户自定义类型,还可以是空类型(void),如果是构造方法,则无返回类型。

    在类图2中,操作method1的可见性为public(+),带入了一个Object类型的参数par,返回值为空(void);操作method2的可见性为protected(#),无参数,返回值为String类型;操作method3的可见性为private(-),包含两个参数,其中一个参数为int类型,另一个为int[]类型,返回值为int类型。

    ​ 图2 类图操作说明示意图

    关联关系

    关联(Association)关系是类与类之间最常用的一种关系,它是一种结构化关系,用于表示一类对象与另一类对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等等。在UML类图中,用实线连接有关联关系的对象所对应的类,在使用Java、C#和C++等编程语言实现关联关系时,通常将一个类的对象作为另一个类的成员变量。在使用类图表示关联关系时可以在关联线上标注角色名,一般使用一个表示两者之间关系的动词或者名词表示角色名(有时该名词为实例对象名),关系的两端代表两种不同的角色,因此在一个关联关系中可以包含两个角色名,角色名不是必须的,可以根据需要增加,其目的是使类之间的关系更加明确。

    如在一个登录界面类LoginForm中包含一个JButton类型的注册按钮loginButton,它们之间可以表示为关联关系,代码实现时可以在LoginForm中定义一个名为loginButton的属性对象,其类型为JButton。如图1所示:

    public class LoginForm { private JButton loginButton; //定义为成员变量 …… } public class JButton { …… }

    比较常见的两种关联关系

    聚合关系

    ​ 聚合(Aggregation)关系表示整体与部分的关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。在UML中,聚合关系用带空心菱形的直线表示。例如:汽车发动机(Engine)是汽车(Car)的组成部分,但是汽车发动机可以独立存在,因此,汽车和发动机是聚合关系,如图6所示:

    ​ 图6 聚合关系实例

    ​ 在代码实现聚合关系时,成员对象通常作为构造方法、Setter方法或业务方法的 参数 注入到整体对象中,图6对应的Java代码片段如下:

    public class Car { private Engine engine; //构造注入 public Car(Engine engine) { this.engine = engine; } //设值注入 public void setEngine(Engine engine) { this.engine = engine; } …… } public class Engine { …… }

    组合关系

    ​ 组合(Composition)关系也表示类之间整体和部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有同生共死的关系。在UML中,组合关系用带实心菱形的直线表示。例如:人的头(Head)与嘴巴(Mouth),嘴巴是头的组成部分之一,而且如果头没了,嘴巴也就没了,因此头和嘴巴是组合关系,如图7所示:

    图7 组合关系实例

    在代码实现组合关系时,通常在整体类的构造方法中直接实例化成员类,图7对应的Java代码片段如下:

    public class Head { private Mouth mouth; public Head() { mouth = new Mouth(); //实例化成员类 } …… } public class Mouth { …… }

    依赖关系

    ​ 依赖(Dependency)关系是一种使用关系,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。大多数情况下,依赖关系体现在某个类的方法使用另一个类的对象作为参数。在UML中,依赖关系用带箭头的虚线表示,由依赖的一方指向被依赖的一方。例如:驾驶员开车,在Driver类的drive()方法中将Car类型的对象car作为一个参数传递,以便在drive()方法中能够调用car的move()方法,且驾驶员的drive()方法依赖车的move()方法,因此类Driver依赖类Car,如图1所示:

    ​ 图1 依赖关系实例

    在系统实施阶段,依赖关系通常通过三种方式来实现,第一种也是最常用的一种方式是如图1所示的将一个类的对象作为另一个类中方法的参数,第二种方式是在一个类的方法中将另一个类的对象作为其局部变量,第三种方式是在一个类的方法中调用另一个类的静态方法。图1对应的Java代码片段如下:

    public class Driver { public void drive(Car car) { car.move(); } …… } public class Car { public void move() { ...... } …… }

    泛化关系

    ​ 泛化(Generalization)关系也就是继承关系,用于描述父类与子类之间的关系,父类又称作基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的直线来表示。在代码实现时,我们使用面向对象的继承机制来实现泛化关系,如在Java语言中使用extends关键字、在C++/C#中使用冒号“:”来实现。例如:Student类和Teacher类都是Person类的子类,Student类和Teacher类继承了Person类的属性和方法,Person类的属性包含姓名(name)和年龄(age),每一个Student和Teacher也都具有这两个属性,另外Student类增加了属性学号(studentNo),Teacher类增加了属性教师编号(teacherNo),Person类的方法包括行走move()和说话say(),Student类和Teacher类继承了这两个方法,而且Student类还新增方法study(),Teacher类还新增方法teach()。如图2所示:

    ​ 图2 泛化关系实例

    //父类 public class Person { protected String name; protected int age; public void move() { …… } public void say() { …… } } //子类 public class Student extends Person { private String studentNo; public void study() { …… } } //子类 public class Teacher extends Person { private String teacherNo; public void teach() { …… } }

    接口与实现关系

    在很多面向对象语言中都引入了接口的概念,如Java、C#等,在接口中,通常没有属性,而且所有的操作都是抽象的,只有操作的声明,没有操作的实现。UML中用与类的表示法类似的方式表示接口,如图3所示:

    ​ 图3 接口的UML图示

    接口之间也可以有与类之间关系类似的继承关系和依赖关系,但是接口和类之间还存在一种实现(Realization)关系,在这种关系中,类实现了接口,类中的操作实现了接口中所声明的操作。在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示。例如:定义了一个交通工具接口Vehicle,包含一个抽象操作move(),在类Ship和类Car中都实现了该move()操作,不过具体的实现细节将会不一样,如图4所示:

    ​ 图4 实现关系实例

    实现关系在编程实现时,不同的面向对象语言也提供了不同的语法,如在Java语言中使用implements关键字,而在C++/C#中使用冒号“:”来实现。图4对应的Java代码片段如下:

    public interface Vehicle { public void move(); } public class Ship implements Vehicle { public void move() { …… } } public class Car implements Vehicle { public void move() { …… } }
    Processed: 0.011, SQL: 8