Java 学习笔记(1)
Attention: 本文建立在具有一定 C++基础知识的前提上
Reference: Introduction to Java Programming 10th Edition (Y. Daniel Liang)
郑重声明:本文原创,资料引用已在原文相应位置进行标注,感谢为我们无偿提供知识和技术指导的创作者们,转载请注明
Basic Concepts
解释型语言和编译型语言的区别
编译型语言(以C++为例)的编译运行过程
- 预编译(*.c/cpp & *.h —> *.i ):对应gcc/g++命令:
gcc -E [xxx] -o [output.i]
- 展开所有宏定义#define(字符替换);
- 处理所有条件预编译命令(#ifdef、#ifndef、#endif等);
- 处理#include,具体操作是将指向的文件直接插入到文件的这一行(严格遵循上一步的条件);
- 删除所有注释;
- 添加行号、文件标识,以便调试/编译出错时及时指出;
- 保留#pragma指令,以供编译器使用;
- 编译(*.i —> *.s ,即高级语言转汇编语言):对应gcc/g++命令:
gcc -S [xxx] -o [output.s]
- 词法分析、语法分析、语义分析(前端);
- 生成中间代码、优化,生成目标代码(解释型语言无需做此步,生成中间代码后直接对语义执行);
- 编译原理入门:https://www.cnblogs.com/fisherss/p/13905395.html
- 汇编(*.s —> *.o/obj,即汇编语言转机器语言):对应gcc/g++命令:
gcc -c [xxx] -o [output.o/obj]
- 类Unix系统下生成*.o,Windows系统下生成*.obj;
- 链接(*.o —> *.out/exe,链接各种需要的库和其他目标文件):对应gcc/g++命令:
gcc [xxx] -o [output]
- 生成调试文件(供gdb使用,对应gcc/g++命令:
gcc [xxx] -g -o [output]
)- 也支持
-o
参数:-O
(默认)、-O1
(尝试减少代码体积和代码运行时间,但是并不执行会花费大量时间的优化操作)、-O2
(包含 -O1的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化,会花费更多的编译时间当然也会生成性能更好的代码)、-O3
- 也支持
解释型语言的运行过程
前端分析“三件套” + 生成中间代码 + 解释器解释执行
图解
注:字节码是低级语言
可惜Java是“半解释型语言”!
Java程序既要编译为class文件,同时class文件也要经过JVM的解释运行
刚入门暂时略过此处有难度的部分,详细参考黑皮书《深入了解Java虚拟机》
Java虚拟机是一种解释器,是解释Java字节码(*.class)的一种程序,
简单解释过程:运行类加载器将字节码加载到内存中 —-> 运行字节码验证器强制检查Java程序合法性和安全性,不符合安全规范的不予运行 —-> 读取内存中的字节码逐句解释为机器码执行;
Java源文件编译(*.java -> *.class):
javac [*.java]
Java字节码解释运行:
java [className]
Attention: 如果填className=”myclass.class”,那么解释器会去找
myclass.class.class
!所以搞清类名;字节码运行提醒:
- Java指令默认寻找class文件通过环境变量
CLASSPATH
指定目录进行,大部分情况下当前目录不在其中,通常需要在其中添加条目.;
- Java对class文件执行强依赖于package路径。运行时严格按照
pwd
+文件中的包名+类名搜索class文件; - 一般大项目没有这么运行的,因为依赖的源文件较多,建议使用IDE、Maven(管理Java库*.jar(二进制)的存储、分发)等工具;别问为什么没有CMake一样管理编译的工具,也别问为什么C++没有库管理工具(C++是头文件+*.dll/so/a/lib,一般靠官网下载);
如果类在包中,举例:
priv.hello.myPackage
(源码的包导入声明也这么写的),包存在/home/xxx/src/
下,那么需要:CLASSPATH
中存在当前目录(可以按照上面所说添加.;
);- 当前命令行位于src(即包名+类名整体的上一级);
此时不能写
java myclass
(包路径缺失)、不能写java myclass.class
(后缀重复);此时正确写法是:
java priv.hello.myPackage.myclass
- Java指令默认寻找class文件通过环境变量
Java的变量存储机制:既然已经充分了解C++,那么敞开天窗说亮话——
Java 中绝大多数对象都包装的比C++好,不需要指针(也没有),创建对象(new)时,由 JVM 在堆中自动分配动态空间,GC也一般不需要编写者关心。所以绝大多数情况下,接触到的类型对象都是“引用”类型,和Python很像
这种“引用”是可以更改的引用,和C++的“引用类型”不同,此后不再赘述
方法调用及存储、方法中的局部变量等都存放于栈中;
操作系统认知
- 控制、监视系统活动:识别输入、发送输出、跟踪文件动态、控制外部设备
- 分配系统资源:确定某一程序需要哪些计算机资源
- 调度:多道程序设计(multiprogramming)、多线程、并行处理
Java 认知
常用于开发:Web应用程序、服务端应用程序、安卓应用程序等
Java版本:JavaSE(标准版,其他所有版本基础,重点)、JavaEE(企业版)、JavaME(微型版)
目前以 JavaSE 8为例,对应的开发工具包为JDK(全部可以基于命令行),也可使用主流IDE进行编写
Java 基本语法
基本规范
每个Java程序都至少应该有一个class,通常大写开头(区分大小写)、驼峰命名;
Java 的 public类名必须和当前的java类文件(*.java)同名!!!
这意味着一个*.java类文件中只能定义一个同名、public的类
Java程序必须有主方法
main
、语句以分号结尾(同C++);Java注释方法、程序块的大括号划分同C++;
在单一的项目内,哪怕源文件很少,也建议将源文件放入“package”(实质是目录)中来组织项目;
目的:防止同名,限制作用域;增强可读性;提升封装性;
命名:所有字符必须是小写字母,不得有特殊字符;
可以有多层;包导入的时候,每层由“.”隔开;
1
2// com一般约定俗称是公司开发的,还可以是team,或者indi(个人项目)、onem(单人项目)、pers(个人发起的项目,有其他人合作)、priv(私有项目);这里指示包位于:项目根目录/com/hello/myPackage 中。
package com.hello.myPackage;
和C++一样,有次行风格和行尾风格两种规范,各有优点,不建议混用;
1
2
3
4
5
6
7// 不良风格示范
public class Test
{// 次行风格
public static void main(String[] args) {// 行尾风格
System.out.println("Hello, Java!");
}
}
基本程序设计(与C++相同部分省略):基础与数据类型
包引入、包声明
import java.util.Scanner(库名);
package priv.hello.myPackage(包);
- 所有Java程序中,自动隐式导入包
java.lang
,内含System
类、Math
类等,无需手动添加;
变量命名规范、声明、定义
命名常量的定义(C++中的常量const)
1 | // final <TYPE> <NAME> = <VAL>; |
等价于C++中:
1 | const double PI = 3.14159265358979; |
注意:Java的常量和C++的不同!Java常量允许仅定义,后来可以赋一次值,但C++要求常量的定义和初始化必须在一起,如下
1 | final double PI; |
1 | const double PI = 3.14; |
赋值语句、赋值表达式
注:Java中字符串和Python一样,允许直接相加,并且存在整型—->字符串的自动类型转换:
1
2 int a = 10;
System.out.println("Hello" + a + "Java!");
标准输入输出(初认识)
Java 使用 System.in、System.out (类)作为标准输入输出;
可惜不支持控制台直接输入,需要引入包:
java.util.Scanner
,也可通配符导入:import java.util.*;
其中含有
Scanner
类,初始化一个实例对象,以便使用:
1
2
3
4 import java.util.Scanner;
Scanner input = new Scanner(System.in);
double radius = input.nextDouble();输入不是double,或者不能自动类型转换为double时,抛出异常;
除了double以外,还接受:byte、short、int、long、float等类型,有对应的方法;
- 输入输出命令行重定向(类似大部分Linux命令可重定向)
java [className] < input.txt
(输入卡)java [className] > output.txt
java [className] < input.txt > output.txt
数据类型入门
- byte:8 bit(1Byte)含符号整型($-2^72^7-1$);
- short:16 bit(2Bytes)含符号整型;
- int:32bit(4Bytes);
- long:64bit(8Bytes);
- float:32bit(4Bytes);
- double:64bit(8Bytes);
- boolean:官方文档表示大小没有严格规定,有时作为int,有时作为byte,可以将其看作“最窄类型”;
- char:16bit(2Bytes);
- String(java.lang类),详细内容以后叙述
注意:还有一点和C++类似——数值型直接量(literal),书写方法与C++相似:
1 | 0xFFFF; |
数值运算符、增强赋值操作符、自加自减运算符、逻辑运算符(绝大部分与C++相同)
注:Java中有API接口库 Math,,包含方法pow进行幂运算,更多方法自行探索:
1 Math.pow(4, 0.5);
运算符优先级
强制(显式)类型转换、自动类型转换,及其原则(和C++有所不同)
- 支持的自动类型转换:绝大多数是宽类型=>缩窄类型/同等宽窄类型
- char <=> int <=> short <=> byte;
- double => float(反过来不行,double也不能到其他整型类型);
- boolean最窄,不支持绝大多数自动类型转换,甚至强制类型转换也不行;
- int/double +(operator) String => String ,只有使用“+”运算符才可将数值型自动类型转换为String,也可以用专用转换函数;
- 支持的强制类型转换
- char、int、short、byte、double、float间均可;
警告:Java中不允许缩窄类型不显式指明进行初始化/类型转换(这样导致很多与C++操作不同之处,例如没法直接
if(<int>)
来判断整型的真假)
1
2
3
4
5
6
7
8 // Java 不允许:
// float x = 1.12;
// byte y = x;
// if (x) { System.out.println(x); }
// 正确写法:
float x = 1.12f;
byte y = (byte)x;
if (x != 0) { System.out.println(x); }
常见错误:与C++相同
使用未声明/定义/初始化的变量、整型溢出、取整错误等;
常用工具
时间工具
Java System类中提供
currentTimeMillis()
方法,返回从1970年1月1日 00:00:00(UNIX首次发布时间,又称UNIX时间戳-UNIX epoch)至今经历的毫秒数;Math类
Java Math类中提供相当多的数学支持:
[0.0, 1.0)
间的均匀伪随机分布数:Math.random()
;- sin、cos、tan、asin、acos、atan(均以弧度为单位)、toRadians、toDegrees;
- exp、log、log10、pow、sqrt;
- ceil(向上取整)、floor(向下取整)、rint(最接近的整数)、round(四舍五入,相当于
floor(x + 0.5)
); - max、min、abs;
调试
- 通过IDE界面调试(图形化);
- 命令行调试:
jdb
程序(类似C++的gdb
);
基本程序设计:字符与字符串与格式化输出
- 字符 char(2Bytes)
遵循和C++一样的内码规则(ASCII),允许比较、参与数值计算、合并打印等;
- 字符串类 String(java.lang包中的引用类型(和C++的引用是否相同?以后讨论))
常用操作方法:
length()
、charAt(index)
(和其他语言的at索引一样,越界会抛出异常)、indexOf()/lastIndexOf()
、toUpperCase()/toLowerCase()
concat(other_str)
:拼接字符串,返回新值(建议用“+”/“+=”,更方便);
substring(begin, end)
:取子串;
trim()
:去掉两边空白字符,类似Python的strip()
;常用比较方法:
equals(other_str)
(仅看内容是否相等的话,和C++一样,不能用==操作符!因为String是引用类型,只能说明地址是否一致)、equalsIgnoreCase(...)
、compareTo(...)
(返回第一个不同的位置上的字符之差)、startWith(prefix)
、endWith(suffix)
、contains(...)
常用转换方法:
除了之前介绍的“+”operator,还可以用内置包
java.lang
的类Integer
、
Double
中的Integer.parseInt(...)
、Double.parseDouble(...)
字符串和输入输出:Scanner类中的实例方法
next()
专门捕获字符串,遇到空白字符停止(空白字符定义同C++,包括space、制表符、换行符等),不从缓冲区拿走空白字符;还有
nextLine()
,不过遇到回车键停止,和C++一样,注意中间的联用问题;
- 格式化输出:
System.out.printf(<pattern> [args])
格式标记符与Python完全相同(例如%d、%4.2f、%s、%%等,唯一一种不同的是%c、%Nc,同时%后、数字前加负号表示左对齐),只不过不需要“%”外部标记;
基本程序设计:条件与分支
比较操作符、条件表达式和三目运算符
由于Java中不允许缩窄类型不显式指明进行初始化/类型转换,只有boolean类型
if - [else if] - else语句
switch - case - [default]语句
和C++类似,只有特定数据类型才能作为switch的变量:byte、char、short、int、String等;
常见错误
忘记必要的大括号、测试浮点数的相等性等;
基本程序设计:循环(全部与C++相同😂)
while循环
do-while 循环
for 循环
break & continue
同样,不应该使用浮点数作为条件
基本程序设计:方法
由于Java的函数全都包裹在类中,所以将Java的函数都称作方法!
Java不允许分支控件有部分没有返回值,会抛出编译错误。但C++只会编译警告(如果经过没有返回值的分支,则返回一个没有初始化为0/空的对应类型的对象)
定义
1
2
3修饰符 返回值类型 方法名(参数列表) {
方法体;
}方法签名:方法名+参数列表;
调用堆栈结构:同C++
方法重载:注意的点也同C++,注意歧义调用(可以自动类型转换的值)
可惜的是,目前为止Java所有方法的参数传递都是值传递
更可惜的是,Java的方法参数不能指定默认值,只能使用重载实现类似效果
基本程序设计:一维数组
请记住,Java中没有指针,一切数组对象都是引用,其类型名为:
elemType[]
定义
1
2
3elemType[] arrayName; // 此处arrayName是引用类型变量
// 可以使用C++风格定义
elemType arrayName[]; // arrayName仍然是引用类型new操作符创建(和C++不同,只有new操作符才会给数组对象引用分配空间,所以说,new是对象新建的关键字)
1
2
3
4
5elemType[] arrayName = new elemType[size]; // 这里的new仅仅是新的意思,arrayName仍然是对新对象的引用;
// 重点:数组引用变量在没有指向具体存储空间(仅定义)是,内容是null;
elemType[] arrayName2; // 此时arrayName2是null
elemType[] arrayName3 = new double[0]; // 但arrayName3不是null,只是一个长度为0的空数组,也占用空间;和C++不一样,一维数组是包装好的引用类型,存在长度属性:
arrayName.length
,存在自动赋初值(0);元素的访问:
arrayName[index]
;赋初值(初始化语法)
使用new操作符;
1
2double[] array = new double[10];
for (int i = 0; i < 10; ++i) array[i] = i;普通赋值(不得将定义和初始化过程分开!因为普通定义不会为数组分配空间);
1
double[] array = {1, 2, 3,...};
对比C++的数组/指针处理:
1
2double* arr = new double[10]; // 可以后边接上赋初值
double arr[] = {1, 2, 3}; // 中括号中的元素个数可以省略特殊的一维数组:char[]
和C++不一样,char数组末尾不用加 ‘\\0’ 字符、支持直接标准输出打印;
char[] city = {'D', 'a', 'l', 'l', 'a', 's'};
遍历:和C++11标准一样,允许foreach循环
for (double x: arrayName) {...}
复制:记住,Java中的数组对象也是引用类型,和Python一样,和C++不一样;
1
2
3
4// ...(前面已定义并为list1、list2赋值)
// 这样的做法会让list1引用指向list2的空间,
// list1原来空间会被 JVM 自动回收(GC机制)
// list1 = list2;正确的方法有3种:
循环语句复制;
System
类中的“静态方法”(是什么以后介绍)arraycopy()
;该方法使用前请确保目标数组已经分配好空间!
clone方法复制,以后介绍;
数组作为参数传递 / 作为返回值
Java中对数组的参数传递方式,和C++一样,都是引用传递;
同时也允许数组类型作为返回值,也是引用传递(和C++只能返回指针又不一样)
因为Java中的类申请空间在堆中,且有GC机制,故不存在不能返回引用类型的说法;
和Python
*args
类似的可变参数列表C++中没有此类功能;
和Python一样,可变参数需要放在普通形参后面
1
2
3
4
5
6// 例如方法:
public static void test(double... argList) {
// ...
}
// 参数定义:elemType... paramName
// 其中的argList会作为double一维数组处理常用数组操作工具:位于
java.util.Arrays
类中sort() / parallelSort()
:排序与并行排序(多处理器有用);binarySearch()
:要求查找前升序排好、未查找到返回某一个负数:-(该元素应该处于的位置下标+1)
;equals(...)
、fill(...)(以某种方式填充)
;toString(...)
:转换的数组对应字符串含有comma、中括号;
小结:目前用到了哪些java的包?
java.util.Scanner
、java.util.Arrays
java.lang.Double
、java.lang.Integer
、java.lang.Math
、java.lang.String
、java.lang.System
main命令行参数
由于传入String一维数组引用类型,和C++的指针数组不一样,所以不需要
int argc
;和C++不一样的是,
main
函数和普通函数一样,可以在其他类中随意调用;重要提示:Java程序的命令行和C++不同,输入*符号相当于目录通配符,想要表达乘法*的意思,使用双引号括起表示字符串
基本程序设计:多维数组
二维数组为例,
Java中的二维数组比C++中开放,允许每行的数组长度不一样,但需要单独创建;
可以将每行看作一个独立的数组引用,可能具有不同的length参数值,自身的length就是行数;
更高维数组同理;
定义
1
elemType[][] arrayName;
创建对象
1
2
3
4
5
6
7
8
9
10
11// 至少指定rowNum,如果没有指定colNum,那么只为每一行的“指针分配空间,总体数组还是null组成的数组”,需要每行去new
double[][] arrayName = new double[rowNum][[colNum]];
// arrayName[i] = new double[j]; ...
// 或者普通创建:
double[][] raggedArray = {
{1, 2, 3, 4, 5},
{1, 2, 3, 4},
{1, 2, 3},
{1, 2},
{1}
};
基本程序设计:面向对象初步
Java 中所有的类都是引用类型、对象都是引用类型变量;
注意:Java的类结尾无需以分号结束类的声明;
统一建模语言(UML)和UML类图
- 类和对象框图的画法;
- “-”:私有修饰符;“+”:公共修饰符;“#”:保护修饰符;
- 下划线:静态成员(静态变量+静态方法);
- 属性:
attrName: type
; - 方法:
method(params: type): returnType
;
主类:包含main方法的类是主类;一个*.java 文件中至多只能含有一个主类、公有类(public,含义见“Java的类可见性修饰符”);
每个类(哪怕不是主类)会在编译时生成对应的*.class文件;
一个*.java文件中,可以没有主类:像C++的面向对象一样,只是为了描述一个类、供外部调用而创建
类所书写的文件名必须和类的公有类同名
类的构造函数(在使用new操作符时被调用)和默认构造函数(同C++)
别问为什么没有析构函数,问就是Java包装太好了,以致于编程人员几乎不涉及GC的处理;
引用属性(引用数据域)和 Java 的 null 值 和 属性的默认值
Java 的“引用”和C++不同,可以修改指向对象,因此可以作为类的属性;
Java 中引用类型中有直接量
null
,就像boolean数值类型有直接量true
、false
;和C++一样,建议一定义对象就进行初始化/赋值操作
- 数值类型的默认值为0、false等;
- 引用类型的默认值为null(直接使用会抛出
NullPointerException
异常);
匿名对象:创建后不需要引用的对象,可以不用赋给引用类型变量:
1
System.out.println("Area is: " + new Circle(5).getArea());
类的静态变量(C++中的静态数据成员、Python中的类属性)、静态常量、静态方法;
- 声明方法与C++相同、实际的储存方法和C++也相同(与Python不同,因为整个类中共享静态数据域);
提示1:静态常量应该在类内定义:
final static <TYPE> name = value;
提示2(其他编程语言一样):调用静态属性/方法时,建议使用类名+方法名/属性,因为能够提高可读性,帮助其他维护者迅速找到静态成员;
提示3:由于Java的类的使用特殊性,类的静态变量也在类中直接初始化(C++不允许,需要类外声明赋值);
Java的类可见性修饰符
private、protected、public
:含义与C++完全相同,但使用方法不同(Java只能声明在属性/方法一行前);- 注意:Java若没有声明可见性修饰符,默认的可见范围是:
package-private
,即在所有同一个包中的所有位置都能被访问,哪怕类不同(C++默认private)
Java中的包能对类本身的”可见性“进行约束(进一步实现对类的组织),可能类似C++的命名空间;
每个文件前可以声明类所处的包,表示将该类放入该包中;如果不指定,则会放入默认包中(不建议这样,因为可能会造成一些混乱);
类的访问器和修改器:Java中的类的封装
这里与Python的属性装饰器中的@set、@get思路有些类似;
对象作为方法参数传递:与前面的”数组作为方法参数传递“完全相同;
对象数组:储存引用的数组;
不可变类和不可变对象(和Python思路相似)
不可变类的定义:一个类满足如下三个条件:
- 所有数据域都是私有的;
- 没有修改器方法;
- 没有”返回值是指向可变数据域的引用”的访问器方法;
因此如果有一个类数据域都私有、没有修改器方法,但有一个方法:返回内部一个可变数据域的引用(例如数组),则这个类也是可变类
Java类中的“this”
只可惜Java中没有指针,所以this是一个引用(使用场景和存在位置都和C++类似);
Java中和C++在这个部分中实现较大的差别是:“委托构造函数”,C++的委托构造函数有专门的语法;但Java的“委托构造函数”只能使用this实现:
this(...)
;其实C++中也可以使用这种方法;另外C++用这种方法可以实现“后部分的委托构造”;
变量作用域:和C++一样的理解;
别问为什么和C++介绍的位置不一样(C++一般在介绍函数之后就介绍,但Java在类的知识中介绍),因为Java几乎所有的操作都在类里面;
使用 Java 内置类型(常见)
java.util.Date
类:提供与系统无关的对时间、日期的封装(不是java.System.currentTimeMillis()
)1
2
3
4
5
6
7
8java.util.Date
-------------------------------------------------------------
+Date() // 为当前时间创建一个Date对象
+Date(elapseTime: long) // 从GMT 1970.1.1至今以毫秒计算的时间创建Date对象
+toString(): String
+getTime(): long // 返回GMT 1970.1.1至今的毫秒数
+setTime(elapseTime: long): voidjava.util.Random
类:提供各种数据类型下的伪随机数1
2
3
4
5
6
7
8java.util.Random
-------------------------------------------------------------
+Random() // 以当前时间为时间种子创建一个Random对象
+Random(seed: long)
+nextInt(): int
+nextInt(n: int): int // 返回一个0~n间(不包含n)的随机int值
// 类似还有nextDouble、nextLong、nextFloat、nextBoolean等提示(其他编程语言一样):可以指定相同种子、生成相同的伪随机序列,这种能力在软件测试中非常有用;
javafx.geometry.Point2D
类:请自行探索;Java的基本数据类型包装为对象(
java.lang.xxx
,可以是Double、Integer、Byte、Character、Boolean、Float、Short、Long)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16template <class T, srcT>
java.lang.T
-------------------------------------------------------------
-value: srcT
+MAX_VALUE: srcT (static)
+MIN_VALUE: srcT (static)
-----------------------------
+T(value: srcT)
+T(s: String)
+otherSrcTValue(): otherT
+toString(): String
+valueOf(s: String): T (static)
// radix: 基数
+valueOf(s: String, radix: int): T (static)
+parseSrcT(s: String): srcT (static)
+parseSrcT(s: String, radix: int): srcT (static)Java内置高精度运算:
java.math.BigDecimal/BigInteger
内部自定义运算名称(add、multiple等),可以与String转换(“”+~可以转为String)
注意
divide
:高精度数除不尽时可能触发ArithmaticException
,需要明确给定保留小数位和舍入方式:a.divide(b, 20, BigDecimal.ROUND_UP)
基本程序设计:面向对象思想和应用
重载:Java不支持运算符重载;
封装
关联、组合
类的关联关系:两个类之间有特定的活动联系;
最简单的例子是:教师类和同学类的关系(教师教授一些课程类、学生学习一些课程类,而且学生与课程、教师与课程间有数量关系限制);
关联的实现方法:两个类使用同类数据域、相关方法;
这里的数量限制又称类的多重性;
补充UML类图:
1
2
3
4
5
6
7
8// [near]m..n[far]: far类需要与m~n(包含)个near类建立关联
// *: far类可以与任意个数个near类建立关联
// x: far类必须与x个near类建立关联
// relation ▶: 类的抽象关系
take ▶ ◀ teach
3..50 * 0..3 1
student --------------- course ------------------ teacher类的聚集和组合
聚集:两个类之间具有归属关系(
has-a
),一个该类的对象可以被其他多个聚集类的对象所归属;组合:如果一个类的对象仅被一个聚集类的对象所归属,则称这两个类的聚集关系为组合;
最典型的例子是:学生-ID-班级类的关系,ID对象唯一地被学生对象所归属,班级对象可以与多个学生进行聚集;
补充UML类图:
1
2
3
4// [near]◆[far]: far和near组合,其中near是owner;(◇代表聚集)
1 1 5..60 1
ID -------------◆ student ◇-------------- Class
继承
UML类图中以指向父类的三角箭头来表示两个类间的继承关系;
继承的写法:
1
[qualifiers] class subClassName extends superClassName {...}
能够继承的部分:父类所有数据域和方法;但能够访问的数据域和方法只有public(依可见性修饰符而定,同C++)——即子类包裹父类,比父类具有更多信息;
Java 中的单一继承和多重继承:和C++和Python不一样:Java不允许多重继承,仅允许单一继承,即一个Java类只能直接继承自一个父类,不能继承于多个父类;
Java 的 super 关键字:和C++不同、和Python类似,Java使用
super
关键字调用父类构造方法C++:子类构造函数自动调用父类构造函数;
Java:想要调用父类构造函数(一般情况下都需要,否则不需要继承了),必须使用super关键字,且要写在子类构造函数的最前面(不写的话编译器会添加默认的
super()
,如果父类恰好没有默认构造函数,则抛出异常);Python 和 Java 类似,只不过是魔法方法
__init__()
,是初始化方法而非构造函数(Python甚至比Java还要少一个考虑空间分配的问题):1
2
3def __init__():
super().__init__(...)
# Statement这些语言构造父类属性时,依此调用父类构造函数的过程称为“构造方法链”
Java 允许通过 super 关键字调用父类public/protected方法:
super.methodName(...)
Java的公共基类:
java.lang.Object
(Java所有类继承于Object类)与C++不同,C++没有公共基类的说法;
和Python2类似,Python2的旧式类的定义就是说明具有公共基类Object;
java.lang.Object
的重要公有方法:toString()
返回类名+@+内存地址构成的十六进制数字符串,可以在Object的所有子类中重写这个方法,类似Python的魔法方法:__str__()
;equals(object x)
:返回两对象引用是否指向同一内存地址,可以在子类中重载来完成仅对内容相同的判断;易错:重写equals的时候,不能改变参数类型!!!
方法重写(override):和C++一样,仅覆盖子类可见的方法;
a. 静态方法不能被覆盖,只会被隐藏;
b. 可以通过
super
关键字调用直接父类的被覆盖的方法!!!c. 建议添加重写标注
@Override
,意义和作用与C++的override
后置关键字完全相同UML类图的书写
- 子类指向父类的实线+空心三角箭头;
- 子类可省略父类中的同签名的方法;
多态
请回想C++的多态实现:基类指针+虚函数(作用是让基类指针调用虚函数时检查子类有没有重写情况,有则调用重写的方法,从而实现多态);
Java 中没有指针,那么说明Java中的父类引用可以指向子类来充当“基类指针”(声明类型和实际类型可以不一致的特性)、Java的重写直接代替“虚函数”的说法;
总结:为什么Java中没有C++的虚函数说法、C++没有Java的super关键字,却都能实现对应功能(例如多态)?
- C++默认基类指针访问非虚函数时,只能访问基类对应的方法,只有加
virtual
关键字才能实现真正检查、覆盖; - 相较于Java,Java默认指向子类对象的父类引用直接可以访问子类的重写方法(即直接覆盖——这里体现Java的引用调用重写方法时完全由实际类型决定,这称为“动态绑定”,C++使用虚函数关键字实现基类指针对该方法的“动态绑定”),但想要访问父类的被覆盖方法,需要加上super关键字指明调用父类;
(tips. Python和Java的多态实现是一样的思路)
- C++默认基类指针访问非虚函数时,只能访问基类对应的方法,只有加
父类-子类类型转换 和 自定义类型转换
子类 —-> 父类:直接转换,因为子类实例总是父类的实例;
父类 —-> 子类:一般不支持!而且 Java不支持自定义类型转换函数!
只有父类引用指向子类对象时才可以使用,且需要显式类型转换,而且转换前要检查是不是真的可以:
instanceof
关键字(<obj> instanceof <class> -> boolean
)Java 不支持自定义类型转换函数;
阻止类扩展和重写
- Java 在类前的修饰符
final
表示:该类不能作为父类(禁止扩展); - Java 在方法前的修饰符
final
表示:该方法不能被重写(与C++ 11的final
后置关键字含义相同);
- Java 在类前的修饰符
字符串类(
java.lang.String
)的进一步分析创建方法及其含义
1
2
3
4
5
6
7// 字符串有两种创建方式:
// 常量赋值法:
// 原理(和C++一样):在常量存储区创建常量字符串"12345",并将引用对象str指向该字符串;
String str = "12345";
// 创建对象法(普通变量使用):
// 原理:在堆中创建一个字符串(也是常量,不过是动态分配的空间),并将str指向该量
String str = new String("12345");String是一种不可变数据类型 && String的限定字符串
原理见上;
限定字符串的工作和Python一模一样。如果是使用常量赋值法定义的两个字符串值相同,那么这两个字符串引用对象共用一个字符串数据内容,称为“限定字符串”
转换为char array:
String.toCharArray()
格式匹配:
replace、replaceAll、replaceFirst、split、matches、equals
( 全部支持正则表达式);格式化生成字符串:
format()
,类似printf的作用,将不同参数的变量转化为字符串;可修改的String区域(和C++的string、char array很像):
java.lang.StringBuilder/StringBuffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21java.lang.StringBuilder
-----------------------------------------------------------------
+StringBuilder()
+StringBuilder(capacity: int)
+StringBuilder(s: String)
// 以下操作会直接修改原数据
+append(...): StringBuilder
+delete(indexes...): StringBuilder
+insert(...): StringBuilder
+replace(...): StringBuilder
+reverse(): StringBuilder
+setCharAt(...): void
+toString(): String
+capacity(): int
+charAt(index: int): char
+length(): int
+setLength(newLength: int): void
+substring(...): String
+trimToSize(): void // shrinkStringBuffer
和StringBuilder
的使用方法相同,区别是前者支持并发操作,后者仅支持普通操作,但性能更好;字符串池:Java 的 String 类会维护一个独立的字符串常量池(JDK7以后,存在于堆内存中)
Java 维护静态变量、常量的方法与C++不同,将常量池和静态变量存在于堆区,因为不用编写者考虑内存分配问题;
C++对于静态局部变量、静态数据成员、全局变量都存在于全局数据区,独立于栈区和堆区;
常量池存储的是引用!!!
代码中的字符串常量(“…”,类似于C++中说的常量表达式constexpr)在运行时直接加载到常量池中(用new创建就不会);
控制局部变量(存于栈帧中)转到常量池:
str2 = str1.intern();
如果
str1
是个局部变量(即:其指向的字符串存在于栈帧中)或者是手动new分配的String,等一切:指向数据不在字符串常量池中的str1
,那么在运行上面一句后,Java会在常量池中加入一个引用(赋给str2
),指向str1
所指向的数据;否则,
str2
直接指向str1
所指向的数据;
基本程序设计:泛型类
定义方法:直接在类名后加类型参数即可:
1
public class stack<E> { ... }
Java的泛型类运行效率极差,且仅支持引用类型E,比C++的模板类慢很多,不建议应用此方法;
实例:
ArrayList
类ArrayList
类可以动态分配列表大小,无需关系容量问题,类似 C++ STL 线性表容器;很遗憾!Java 的泛型类仅支持引用对象,而非基本数据类型作为类型参数!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18java.util.ArrayList<E>
--------------------------------------------------------------------------
+ArrayList() // 线性数据结构(线性表,甚至杂糅一些集合结构的内容在其中)
+clear(): void // 清
+add(obj: E): void // 增
+add(index: int, obj: E): void
+remove(obj: Object): boolean // 删
+remove(index: int): E
+contains(obj: Object): boolean // 查
+get(index: int): E
+indexOf(obj: Object): int
+lastIndexOf(obj: Object): int
+set(index: int, obj: E): E // 改
+isEmpty(): boolean // 判空
+size(): int
+toString(): String从对象数组创建ArrayList:
java.util.Array.asList(object[] arr(是引用)) -> 数组数据
从ArrayList复制普通数组:
ArrayListObj.toArray(object[] arr)
,要求arr已分配空间;支持
ArrayList
的Java库:java.util.Collections
max / min、shuffle、sort方法等;
基本程序设计:异常处理
抛出异常:例如:
throw new
)try-catch 块:与C++相同;
Java 中的异常类
图中的几个重要概念:
异常基类
Throwable
和常用异常类Exception
1
2
3
4
5
6
7
8 java.lang.Throwable
--------------------------------------------------------------------
+getMessage(): String
+toString(): String
+printStackTrace(): void
+getStackTrace(): StackTraceElement[]
Exception
类继承自Throwable
,以上方法都具有,故省略:
1
2
3
4 java.lang.Exception
--------------------------------------------------------------------
+Exception()
+Exception(message: String)系统错误
system error
:Java虚拟机抛出的异常,内部系统有错误,遇到后必须立即终止程序不能有补救,只能有善后;LinkageError:依赖库已修改,不兼容当前代码;
VirtualMachineError:Java虚拟机崩溃,或者运行时所需资源耗尽;
异常
exception
:可以被捕获和处理的异常ClassNotFoundException:
java
命令运行的类无法找到;IOException:包括子类
InterruptedIOException
、EOFException
、FileNotFoundException
等;运行时异常(异常的子集)
runtime exception
;
Java 异常处理机制
声明异常(关键字
throws
):任何方法必须尾置声明可能抛出的异常(RuntimeException
、Error
及其子类除外,如IOException
、自定义类就就必须声明,除非当场使用try-catch捕获):1
public void testMethod() throws IOException [, ...] { ... }
抛出异常(关键字
throw
)throw ExceptionObj;
1
throw new ArithmeticException('...');
捕获异常(try-catch-finally):语法几乎与C++相同、
finally
关键字用法同Python(即使前面有return也会进行);JDK 7 多捕获特性:
1
2try { ... }
catch (exception1 | exception2 | ...) { ... }
使用须知
- 没有被捕获的异常会导致程序中断;
- 由于异常调用栈回溯繁琐,会耗费更多资源,故不应过度使用异常;
- 必要时重新抛出异常、设计链式异常;
自定义异常类:建议基于
Exception
类做扩展(其UML类图见上);
基本程序设计:Java的抽象类和接口
提示:看源码的时候,关键字
native
表示这个方法不是用Java写的,但是基于JVM平台实现的;
抽象类的定义方法
1
[qualifiers] abstract class className { ... }
抽象方法
- 定义:
[qualifiers] abstract <returnType> funcName(params);
- 类似C++的纯虚函数;
- 定义:
抽象类的构造函数
和C++一样,逻辑上抽象类不需要定义构造函数,但如果执意要定义,那么需要声明为protected类型(C++也一样,思考为什么);
注意:Java 在子类中不能像C++一样自动调用父类的构造函数,所以抽象类的子类和普通类的子类一样,都需要显式super调用父类的构造函数;
抽象类/方法的UML类图表示:文字以 斜体 表示;
注意事项
和C++一样,Java不允许实例化抽象类;
包含抽象方法的类一定要是抽象类;
- 与C++不同,Java因为会抽象声明的原因,其抽象类可以没有抽象方法,但建议有;
- Java 的一个具体类的子类可以是抽象类(例如Object是具体类,而我们能够定义抽象子类);
- 抽象方法一定不是静态的;
抽象类的一些实例
java.lang.Number
:Double、Float、Long、Integer、Short、Byte、BigInteger、BigDecimal都是其子类;java.util.Calendar
:其他类型的历法方式是其子类;
接口的定义和使用
1
2
3
4// 定义
[qualifiers] interface InterfaceName {} // 允许泛型接口!
// 使用
[qualifiers] class className [extend ...] implements InterfaceName { ... }接口 的 注意事项
和抽象类一样抽象:描述一群类的共同行为 / 常量数据,类似Python的“协议”(如上下文管理协议、序列协议等);
仅包含常量、抽象方法:数据域全是
public static final
,方法全是public abstract
,故接口定义中允许省略所有修饰符;C++ 的“接口”一般用抽象类实现就行,因为C++允许多继承;
接口允许继承:
1
public interface interfaceName extend I1 [, ...] { ... }
类允许接口的多重扩展:
1
class className implements i1 [, ...] { ... }
UML类图:虚线+空心三角形+
<<interface>>
接口的一些实例
java.lang.Comparable
:只有一个方法compareTo(E obj);
java.lang.Cloneable
:空接口(标记接口),标记有此接口的对象可以重写Object.clone
以完成功能的实现;
基本程序设计:文本 I/O 和网络 I/O
File
类:此类初始化后并没有真正打开对象(占用资源)!不需要释放1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27java.io.File
--------------------------------------------------------------------
+File(pathName: String)
+File(parentDir: String, childPath: String)
+File(parentDir: File, childPath: String)
// 谓词函数,默认返回值为boolean,省略
+exists()
+canRead()
+canWrite()
+isDirectory()
+isFile()
+isAbsolute() // File 对象的path数据是否是绝对路径
+isHidden() // Windows中的隐藏属性,Unix中文件前的“.”
+getAbsolutePath(): String
+getCanonicalPath(): String // 将path数据转换为标准的绝对路径(没有简写)
+getName(): String // 相当于Python的os.path.basename(...)+后缀
+getParent(): String // 相当于Python的os.path.dirname(...)
+lastModified(): long // 文件最后修改时间(从Unix时间开始的毫秒数)
+length(): long // 文件大小(如果File对象指向目录或不存在,为0)
+listFile(): File[] // 相当于Python的os.listdir()
+delete(): boolean // 删除File对象指向的文件/目录
+renameTo(dst: File): boolean
+mkdir(): boolean
+mkdirs(): boolean文本输入:
java.io.PrintWriter
:此类对象初始化后会占用文件资源,记得及时释放1
2
3
4
5
6
7
8
9
10
11java.io.PrintWriter
--------------------------------------------------------------------
+PrintWriter(file: File)
+PrintWriter(filename: String)
+print(s: String): void
+print(c: char): void
+print(cArray: char[]): void
// 注:此外还支持其他各种基本数据类型、包含println(自动换行)、printf(格式化输出)重载方法!!!
+close(): voidJava 中类似 Python 上下文管理器 处理I/O的工具:JDK 7 的
try-with-resources
语法1
2
3try (声明、创建资源) {
使用资源;
} // 下面(Scanner)也能用,就是try-catch的功能之一!文本输入 && 控制台输入:
java.util.Scanner
(第三次介绍)1
2
3
4
5
6
7
8
9
10
11
12java.util.Scanner
--------------------------------------------------------------------
+Scanner() // 默认标准输入,即控制台
+Scanner(source: File) // 注意:可能抛出 IOException !!!
+Scanner(source: String)
+close() // 提示:如果是用来访问文件的,及时释放;控制台输出则不必要;
+hasNext(): boolean
+next(): String // 分隔符为空格时,如果遇到换行符,则立即停止(读指针停在\n前)
+nextXXX(): XXXX // XXX表示各种基本数据类型
+nextLine(): String
+useDelimiter(pattern: String): Scanner // 指定分隔符,默认空格Java 的网络I/O:以简单爬虫为例
Java 的目标之一是互联网应用,所以对于网络相关部件封装的会比其他一些语言更好
java.net.URL
类:管理对URL的访问的类,仅介绍一个方法:URL.openStream() -> inputStream(和System.in一个类型,都是输入流类型,类似C++的istream)
,因为都是输入流,可以采用Scanner读取数据;爬虫示例:详见我的另一篇博客《开发一个极简的 Java 网络爬虫》
基本程序设计:二进制 I/O
二进制I/O比文本I/O更高效:无需进行字符的编解码;
Java 的*.class文件就以二进制形式存储;
- Java 中几乎所有 I/O 方法都可能
throws IOException
;- 个人主观感觉,Java的二进制 I/O 比Python、C++的好用…
- 注意
EOFException
的处理;
Binary I/O 类:和
PrintWriter
、Scanner
一样,会占用资源,请及时释放摘自 Introduction to Java Programming 10th Edition (Y. Daniel Liang)
1
2
3
4
5
6
7
8
9
10
11
12
13
14java.io.InputStream (abstract)
------------------------------------------------------------------------
+read(): int (abstract) // 从输入流读取下一个字节(0~255的int表示),EOF则返回-1
+read(b: byte[]): int // 从输入流读b.length()个字节,返回实际读到的字节数
+read(b: byte[], off: int, len: int): int // 保存在b[off], b[off+1], ..., b[off+len-1]中
+available(): int // 目前可读的字节数
+close(): void // 释放资源
+skip(n: long): long // 跳过、丢弃n个字节,返回实际跳过的字节
+markSupported(): boolean // 是否支持以下两个方法
+mark(readlimit: int): void // 标记当前位置(默认开头)
+reset(): void // 将当前读指针定位到之前标记的位置1
2
3
4
5
6
7
8
9java.io.OutputStream (abstract)
------------------------------------------------------------------------
+write(b: int): void (abstract) // 将(byte)b写入输出流
+write(b: byte[]): void
+write(b: byte[], off: int, len: int): void
+close(): void
+flush(): void // 注:缓冲区何时自动更新和C++一样向文件中读写二进制数据:
1
2
3java.io.FileInputStream
------------------------------------------------------------------------
// 省略同签名函数就没什么东西了,唯一不同的是构造函数(可名可File),读没有花样,简单(不存在会报错)1
2
3java.io.FileOutputStream
------------------------------------------------------------------------
// 构造函数类似文件输入流(上面),只是有一个额外的参数:append,决定是否追加还是覆盖(不存在会创建)过滤二进制数据(从二进制数据中翻译为基本数据类型):
1
2
3
4
5
6
7<<interface>>
java.io.DataInput (abstract)
------------------------------------------------------------------------
+readXXX(): XXX // 读取XXX的基本数据类型
+readLine(): String
+readUTF(): String // 以UTF格式读为字符串 (注:UTF-8格式中,每个字符1Byte)
// 这个接口对标准输入也通用!1
2
3
4
5<<interface>>
java.io.DataOutput (abstract)
------------------------------------------------------------------------
// 把上面接口中的read改为write
+writeChars(s: String): void // 每个字符2ByteDataInputStream
、DataOutputStream
就是配备以上两个接口、以InputStream、OutputStream为间接父类的应用类;缓冲区操作二进制数据:
BufferInputStream
、BufferOutputStream
,使用见InputStream、OutputStream,唯一不同的是初始化时建议给定缓冲区大小;应该总是采用缓冲区来提升 I/O 速度,尤其大文件更能感受到;
随机访问文件(与之前的顺序流不同):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15java.io.RandomAccessFile
// 以DataInput、DataOutput为接口
------------------------------------------------------------------------
+RandomAccessFile(file: File, mode: String) // mode 与 Python规则一样
+RandomAccessFile(name: String, mode: String)
+close(): void
+getFilePointer(): long // 没有指针,只有字节数偏移量
+length(): long
+read(): int
+read(b: byte[] [,...]): int
+write(b: byte[] [,...]): void
+seek(pos: long): void // 设置指针为从流开始位置起的偏移量
+skipBytes(int n): int对象 I/O 和序列化接口
ObjectInputStream
基于接口ObjectInput
(其基于DataInput
)和ObjectStreamConstants
;ObjectOutputStream
同理;现阶段只需会用即可:
readObject() -> Object
、writeObject(Object x)
能够序列化的对象必须配备
serializable
接口(一种标记接口)不支持则抛出
NoSerializableException
Java 中大多数基本数据类型、数组、字符串、大数类、日期类等都可序列化;
简单的自定义可序列化类
1
2
3
4
5
6
7public class example implements java.io.Serializable {
// 必须全由可序列化的数据域组成!
// 如果有不可序列化的数据域,需要使用transient关键字丢弃它
private transient unSerializableClass u;
// 不会序列化静态变量
private static double x;
}
基本程序设计:总结
前面使用到的所有类、包:复习回顾有哪些知识?(主要)
类的设计原则
内聚性、一致性(名字一致等)、封装性、清晰性、完整性、实例静态区分、继承聚会结合、灵活使用接口和抽象类;
全文完——————————————————————-