标识符是用来给变量、类、方法以及包进行命名的。4大规则:
标识符的使用规范
- 表示类名的标识符:每个单词的首字母大写,如
Man,GoodMan- 表示方法和变量的标识符:第一个单词小写,从第二个单词开始首字母大写,我们称之为“驼峰原则”,如
eat(),eatFood()
Java不采用ASCII字符集,而是采用Unicode字符集。因此,这里字母的含义不仅仅是英文,还包括汉字等等。但是不建议大家使用汉字来定义标识符!
变量有三种类型:局部变量、成员变量(也称为实例变量)和静态变量。
局部变量、成员变量、静态变量的核心区别
| 类型 | 声明位置 | 从属于 | 生命周期(作用域) |
|---|---|---|---|
| 局部变量 | 方法或语句块内部 | 方法/语句块 | 从声明位置开始,直到方法或语句块执行完毕,局部变量消失 |
| 成员变量(实例变量) | 类内部,方法外部 | 对象 | 对象创建,成员变量也跟着创建。对象消失,成员变量也跟着消失; |
| 静态变量(类变量) | 类内部,static修饰 | 类 | 类被加载,静态变量就有效;类被卸载,静态变量消失。 |
成员变量(也叫实例变量 member variable)【暂不用掌握,讲面向对象再说】
方法外部、类的内部定义的变量。从属于对象,生命周期伴随对象始终。如果不自行初始化,它会自动初始化成该类型的默认初始值。
| 表2-3 实例变量的默认初始值 | |
|---|---|
| 数据类型 | 实始值 |
| int | 0 |
| double | 0.0 |
| char | ‘\u0000’ |
| boolean | false |
· 静态变量(类变量 static variable)【暂不用掌握,讲面向对象再说】
使用static定义。 从属于类,生命周期伴随类始终,从类加载到卸载。 (注:讲完内存分析后我们再深入!先放一放这个概念!) 如果不自行初始化,与成员变量相同会自动初始化成该类型的默认初始值,如表 2-3所示。
在Java语言中,用关键字final来定义一个常量。常量一旦被初始化后不能再更改。
声明格式:
final type varName = value;
为了更好的区分和表述,一般将1、2、3、’a’、’b’、true、false、helloWorld等称为字符常量,而使用final修饰的PI等称为符号常量。
变量和常量命名规范
monthSalaryMAX_VALUEMan, GoodManrun(),runRun()
Java数据类型分为两大类:基本数据类型(primitive data type)和引用数据类型(reference data type)。
注意事项
- 引用数据类型的大小统一为4个字节,记录的是其引用对象的地址!

| 类型 | 占用存储空间 | 表数范围 |
|---|---|---|
| byte | 1字节 | -27 ~ 27-1(-128~127) |
| short | 2字节 | -215 ~ 215-1 (-32768~32767) |
| int | 4字节 | -231 ~ 231-1 (-2147483648~2147483647) 约21亿 |
| long | 8字节 | -263 ~ 263-1 |
Java 语言整型常量的四种表示形式

整形常量默认为int类型。
Java语言的整型常数默认为int型,声明long型常量可以后加l或L 。
【示例】long类型常数的写法及变量的声明
long a = 55555555; //编译成功,在int表示的范围内(21亿内)。
long b = 55555555555; //不加L编译错误,已经超过int表示的范围。
报错:The literal 55555555555 of type int is out of range,所以我们需要修改代码为:
long b = 55555555555L;
| 类型 | 占用存储空间 | 表数范围 |
|---|---|---|
| float | 4字节 | -3.403E38~3.403E38 |
| double | 8字节 | -1.798E308~1.798E308 |
float类型又被称作单精度类型,尾数可以精确到7位有效数字。
double表示这种类型的数值精度约是float类型的两倍,又被称作双精度类型,绝大部分应用程序都采用double类型。
Java浮点类型常量有两种表示形式
(1) 十进制数形式: 例: 3.14 314.0 0.314
(2) 科学记数法形式 例:3.14e0 3.14E2 3.14E-1
浮点型不精确,不要用于比较


上一个例子会出现true的结果原因是:这是因为由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。
浮点数一般都存在舍入误差,很多数字无法精确表示(例如0.1),其结果只能是接近, 但不等于。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。并不是所有的小数都能可以精确的用二进制浮点数表示。
浮点数存在舍入误差,数字不能精确表示。浮点数适合普通的科学和工程计算,精度足够;但不适合精度要求非常高的商业计算,这时候要使用BigDecimal进行运算和比较。
java.math包下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。
浮点常量默认类型是double,要改成float可以后面加F或f

float类型的数值有一个后缀F或者f,没有后缀F/f的浮点数值默认为double类型。也可以在浮点数值后添加后缀D或者d, 以明确其为double类型。

unicode使用变长编码可以给英文省内存空间啥的。

ASCII字符集表示了英文字母、数字、特殊字符、控制符,所有字符集的老祖宗,大家都会兼容它。但是一个字节能够表示256个状态,而ASCII字符只用到128个,后面128个一直是空的。
于是有了ISO8859-1, 别名叫latin-1, 包含了256个字符。前128个字符与ASCII中完全相同。后128个包括了西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。
随着我国的计算机普及,汉字的处理也有了我们自己的方案。那就是GB2312,两个字节表示1个汉字。两个字节可以表示65536个状态,汉字再多也能全部包含。后来,又有了GBK、GB18030。
我国的台湾地区自己搞了一套显示繁体中文的大五码BIG5。
全世界各个地方,都有自己的文字编码。由于不互通,经常造成乱码的问题。
如果有一种统一的字符集,将世界上所有语言字符都纳入其中,每一个字符都给予一个全球独一无二的编码,那么乱码问题就会消失。于是,全球所有国家和民族使用的所有语言字符的统一字符集诞生了,这就是Unicode字符集。
Unicode字符集是为了给全世界所有字符一个唯一的编码,“唯一”对应的英文为Unique,而编码的英文为code。
Unicode采用了字符集和编码分开的策略。Unicode之前,Unicode诞生之前可以将字符集和字符编码混为一谈,而在Unicode中必须严格区分开。
Unicode字符集统一采用两个字节表示一个字符,包括英文字母。但是,由于英文占据互联网信息的绝大部分。真实存储和传输时,会造成极大的浪费;
因此,目前主要采用UTF-8编码来实现具体的存储和传输。UTF-8是变长编码,用1-6个字节编码Unicode字符。西欧字符仍然是1个字节,汉字3个字节。
字符型在内存中占2个字节,在Java中使用单引号来表示字符常量。例如'A'是一个字符,它与"A"是不同的,"A"表示含有一个字符的字符串。
char 类型用来表示在Unicode编码表中的字符。Unicode编码被设计用来处理各种语言的文字,它占2个字节,可允许有65536个字符。
Unicode具有从0到65535之间的编码,他们通常用从’\u0000’到’\uFFFF’之间的十六进制值来表示(前缀为u表示Unicode)
Java 语言中还允许使用转义字符 ‘\’ 来将其后的字符转变为其它的含义。常用的转义字符及其含义和Unicode值如表2-6所示。
| 表2-6 转义字符 | ||
|---|---|---|
| 转义符 | 含义 | Unicode值 |
| \b | 退格(backspace) | \u0008 |
| \n | 换行 | \u000a |
| \r | 回车 | \u000d |
| \t | 制表符(tab),就是表格一样会空格开来 | \u0009 |
| \“ | 双引号 | \u0022 |
| \‘ | 单引号 | \u0027 |
| “\\” | 反斜杠 | \u005c |
注意事项
以后我们学的String类,其实是字符序列(char sequence), 本质是char字符组成的数组。
String是一个类,是一个对象
1.布尔数组每个1字节,布尔变量则4字节;
2.java中的布尔就只有true与false,不能用0或非0的整数代替。
true和false。true和false,这点和C语言不同。【注意点】
JVM规范指出boolean当做int处理,也就是4字节,boolean数组当做byte数组处理,这样我们可以得出boolean类型占了单独使用是4个字节,在数组中是确定的1个字节。

二元运算符的运算规则:
整数运算:
浮点运算:
取模运算:
7%3=1,-7%3=-1,7%-3=1。造成:两个整数相除,直接保留结果的整数部分,没有四舍五入也不会自己从int变成double,除非两个操作数中有一个double
关系运算符用来进行比较运算。
关系运算的结果是布尔值:true/false;
注意事项
=是赋值运算符,而真正的判断两个操作数是否相等的运算符是==。==、!= 是所有(基本和引用)数据类型都可以使用。> 、>=、 <、 <= 仅针对数值类型(byte/short/int/long,float/double 以及char)逻辑运算的操作数和运算结果都是boolean值。

短路与和短路或采用短路的方式。从左到右计算,如果只通过运算符左边的操作数就能够确定该逻辑表达式的值,则不会继续计算运算符右边的操作数,提高效率。
位运算指的是进行二进制位的运算。
| 位运算符 | 说明 |
|---|---|
| ~ | 取反 |
| & | 按位与 |
| | | 按位或 |
| ^ | 按位异或 |
| << | 左移运算符,左移1位相当于乘2 |
| >> | 右移运算符,右移1位相当于除2取商 |
新手雷区
boolean类型,就作为逻辑运算符。如果两侧的操作数是整数类型,就是位运算符。^当做数学运算乘方,是位的异或操作。字符串连接符
+运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串然后再进行连接。



条件运算符

x为 boolean 类型表达式,先计算 x的值,若为true,则整个运算的结果为表达式y的值,否则整个运算结果为表达式z 的值。
运算符的优先级
| 优先级 | 运算符 | 类 |
|---|---|---|
| 1 | () | 括号运算符 |
| 2 | !、+(正号)、-(负号) | 一元运算符 |
| 2 | ~ | 位逻辑运算符 |
| 2 | ++、– | 递增与递减运算符 |
| 3 | *、/、% | 算术运算符 |
| 4 | +、- | 算术运算符 |
| 5 | <<、>> | 位左移、右移运算符 |
| 6 | >、>=、<、<= | 关系运算符 |
| 7 | ==、!= | 关系运算符 |
| 8 | & | 位运算符、逻辑运算符 |
| 9 | ^ | 位运算符、逻辑运算符 |
| 10 | | | 位运算符、逻辑运算符 |
| 11 | && | 逻辑运算符 |
| 12 | || | 逻辑运算符 |
| 13 | ? : | 条件运算符 |
| 14 | =、+=、-=、*=、/=、%= | 赋值运算符、扩展运算符 |
老鸟建议
- 大家不需要去刻意的记这些优先级,表达式里面优先使用小括号来组织!!
- 逻辑与、逻辑或、逻辑非的优先级一定要熟悉!(逻辑非>逻辑与>逻辑或)。
- 如:
a||b&&c的运算结果是:a||(b&&c),而不是(a||b)&&c
我们讲解了八种基本数据类型,除了boolean类型之外的七种类型是可以自动转化的。
自动类型转换指的是容量小的数据类型可以自动转换为容量大的数据类型。如图2-6所示,黑色的实线表示无数据丢失的自动类型转换,而虚线表示在转换时可能会有精度的损失。

自动类型转换图
可以将整型常量直接赋值给byte、 short、 char等类型变量,而不需要进行强制类型转换,只要不超出其表数范围即可。

蓝色虚线精度损失

强制类型转换,又称为造型(cast),用于强制转换数值的类型,可能损失精度。
当将一种类型强制转换成另一种类型,而又超出了目标类型的表数范围,就会被截断成为一个完全不同的值。

操作比较大的数时,要留意是否溢出,尤其是整数操作时。
L和l的问题:
(1) 不要命名名字为l的变量,字母l容易和数字1混淆。
(2) long类型使用大写L,不要用小写l
【示例】类型转换常见问题一
int money = 1000000000; //10亿
int years = 20;
//返回的total是负数,超过了int的范围
int total = money*years;
System.out.println("total="+total);
//返回的total仍然是负数。默认是int,因此结果会转成int值,再转成long。但是已经发生//了数据丢失
long total1 = money*years;
System.out.println("total1="+total1);
//返回的total2正确:先将一个因子变成long,整个表达式发生提升。全部用long来计算。
long total2 = money*((long)years);
System.out.println("total2="+total2);
【示例】使用Scanner获取键盘输入
import java.util.Scanner;
public class Welcome2 {
public static void main(String[ ] args) {
Scanner scanner = new Scanner(System.in);
// 将输入的一行付给string1
String string1 = scanner.nextLine();
// 将输入单词到第一个空白符为止的字符串付给string2
String string2 = scanner.next();
// 将输入的数字赋值给变量
int a = scanner.nextInt();
System.out.println("-----录入的信息如下-------");
System.out.println(string1);
System.out.println(string2);
System.out.println(a * 10);
}
}
switch会根据表达式的值从相匹配的case标签处开始执行,一直执行到break处或者是switch的末尾。如果表达式的值与任一case值不匹配,则进入default语句。switch中表达式的值,是int(byte、short、char也可,long不行)、枚举,字符串。循环结构分两大类,一类是当型,一类是直到型。
当布尔表达式条件为true时,反复执行某语句,当布尔表达式的值为false时才停止循环,比如:while与for循环。
先执行某语句, 再判断布尔表达式,如果为true,再执行某语句,如此反复,直到布尔表达式条件为false时才停止循环,比如do-while循环。

语法结构:
while (布尔表达式) {
循环体;
}

语法结构:
do {
循环体;
} while(布尔表达式) ;
do-while循环结构会先执行循环体,然后再判断布尔表达式的值,若条件为真,执行循环体,当条件为假时结束循环。do-while循环的循环体至少执行一次。

语法结构:
for (初始表达式; 布尔表达式; 迭代因子) {
循环体;
}
for循环在执行条件判定后,先执行的循环体部分,再执行步进。
编译器将while(true)与for(;;)看作同一回事,都指的是无限循环。

break用于强行退出整个循环continue用于结束本次循环,继续下一次“标签”是指后面跟一个冒号的标识符,例如:label:。对Java来说唯一用到标签的地方是在循环语句之前。
goto有害论中,最有问题的就是标签,而非goto,随着标签在一个程序里数量的增多,产生错误的机会也越来越多。
但Java标签不会造成这方面的问题,因为它们的活动场所已被限死,不可通过特别的方式到处传递程序的控制权。
java中只有continue可以使用标签跳转
【示例】带标签的continue
//控制嵌套循环跳转(打印101-150之间所有的质数)
public class Test18 {
public static void main(String args[ ]) {
outer: for (int i = 101; i < 150; i++) {
for (int j = 2; j < i / 2; j++) {
if (i % j == 0){
continue outer; //符合某条件,跳到外部循环继续
}
}
System.out.print(i + " ");
}
}
}
执行结果如图所示:


递归结构包括两个部分:
递归的缺陷
算法简单是递归的优点之一。但是递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
// 每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
对于一个类来说,有三种成员:属性field、方法method、构造器constructor。
属性(field 成员变量)
属性用于定义该类或该类对象包含的数据或者说静态特征。属性作用范围是整个类体。
在定义成员变量时可以对其初始化,如果不对其初始化,Java使用默认的值对其初始化。

属性定义格式
[修饰符] 属性类型 属性名 = [默认值] ;
方法
**方法用于定义该类或该类实例的行为特征和功能实现。**方法是类和对象行为特征的抽象。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。
[修饰符] 方法返回值类型 方法名(形参列表) {
// n条语句
}
构造器用于对象的初始化,而不是创建对象!
构造方法是负责初始化(装修),不是建房子

声明格式:
[修饰符] 类名(形参列表){ //n条语句}
构造器4个要点:

学习内存模型是为了更好理解面向对象
我们前面做过的内存分析过程:

Java虚拟机的内存可以分为三个区域:栈stack、堆heap、方法区method area。
虚拟机栈(简称:栈)的特点如下:
堆的特点如下:

方法区(也是堆)特点如下:
方法区是JAVA虚拟机规范,可以有不同的实现。
i. JDK7以前是“永久代”
ii. JDK7部分去除“永久代”,静态变量、字符串常量池都挪到了堆内存中
iii. JDK8是“元数据空间”和堆结合起来。
JVM只有一个方法区,被所有线程共享!
方法区实际也是堆,只是用于存储类、常量相关的信息!
用来存放程序中永远是不变或唯一的内容。(类信息【Class对象,反射机制中会重点讲授】、静态变量、字符串常量等)
常量池主要存放常量:如文本字符串、final常量值。
Java中,方法中所有参数都是“值传递”,也就是“传递的是值的副本”。 也就是说,我们得到的是“原参数的复印件,而不是原件”。
· 基本数据类型参数的传值
传递的是值的副本。 副本改变不会影响原件。
· 引用类型参数的传值
传递的是值的副本。但是引用类型指的是“对象的地址”。因此,副本和原参数都指向了同一个“地址”,改变“副本指向地址对象的值,也意味着原参数指向对象的值也发生了改变”。

Java引入了垃圾回收机制,令C++程序员最头疼的内存管理问题迎刃而解。Java程序员可以将更多的精力放到业务逻辑上而不是内存管理工作上,大大的提高了开发效率。
垃圾回收原理和算法
· 内存管理
Java的内存管理很大程度就是:堆中对象的管理,其中包括对象空间的分配和释放。
· 垃圾回收过程
任何一种垃圾回收算法一般要做两件基本事情:
垃圾回收机制保证可以将“无用的对象”进行回收。
无用的对象指的就是没有任何变量引用该对象。Java的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。
垃圾回收相关算法
引用计数法
堆中的每个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器加1,而当指向该对象的引用失效时(引用变为null),引用计数器减1,最后如果该对象的引用计算器的值为0时,则Java垃圾回收器会认为该对象是无用对象并对其进行回收。优点是算法简单,缺点是“循环引用的无用对象”无法别识别。

【示例】循环引用演示
代码中,s1和s2互相引用对方,导致他们引用计数不为0,但是实际已经无用,但无法被识别。
public class Student {
String name;
Student friend;
public static void main(String[ ] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.friend = s2;
s2.friend = s1;
s1 = null;
s2 = null;
}
}
引用可达法(根搜索算法)
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、永久代。同时,将处于不同状态的对象放到堆中不同的区域。
1. 年轻代
所有新生成的对象首先都是放在Eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
2. 年老代
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。
3. 永久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。JDK7以前就是“方法区”的一种实现。JDK8以后已经没有“永久代”了,使用metaspace元数据空间和堆替代。

·Minor GC:
用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中。
·Major GC:
用于清理年老代区域。
·Full GC:
用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
内存泄漏:
指堆内存由于某种原因程序未释放,造成内存浪费,导致运行速度减慢甚至系统崩溃等。
老鸟建议
- 在实际开发中,经常会造成系统的崩溃。如下这些操作我们应该注意这些使用场景。 请大家学完相关内容后,回头过来温习下面的内容。不要求此处掌握相关细节。
如下四种情况时最容易造成内存泄露的场景,请大家开发时一定注意:
1.创建大量无用对象
比如:大量拼接字符串时,使用了String而不是StringBuilder。
String str = "";
for (int i = 0; i < 10000; i++) {
str += i; //相当于产生了10000个String对象
}
2. 静态集合类的使用
像HashMap、Vector、List等的使用最容易出现内存泄露,这些静态变量的生命周期 和应用程序一致,所有的对象也不能被释放。
3. 各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭
IO流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网 络连接,不使用的时候一定要关闭。
4. 监听器的使用不当
释放对象时,没有删除相应的监听器
其他要点
创建对象的四步:
this的其他要点:
- this()调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
- this不能用于static方法中。
- this是作为普通方法的“隐式参数”,由系统传入到方法中。
意思就是说,在一个构造函数里面调用另一个构造函数使用this(无参/有参), 并且必须位于构造方法的第一句。
静态变量(类变量)、静态方法(类方法):static声明的属性或方法。
静态变量/静态方法生命周期和类相同,在整个程序执行期间都有效。它有如下特点:
- 为该类的公用变量,属于类,被该类的所有实例共享,在类载入时被初始化。
- static变量只有一份。
- 一般用“类名.类变量/方法”来调用。
- 在static方法中不可直接访问非static的成员

构造方法用于对象的普通属性初始化。
静态初始化块,用于类的初始化操作,初始化静态属性。
在静态初始化块中不能直接访问非static成员。
注意事项
静态初始化块执行顺序(学完继承再看这里):
- 上溯到Object类,先执行Object的静态初始化块,再向下执行子类的静态初始化块,直到类的静态初始化块为止。
- 构造方法执行顺序和上面顺序一样!!
public class TestStatic2 {
static String company; //公司名称
static {
System.out.println("执行类的初始化工作");
company = "北京尚学堂";
printCompany();
}
public static void printCompany(){
System.out.println(company);
}
public static void main(String[ ] args) {
}
}

变量有三种类型:局部变量、成员变量(也称为实例变量)和静态变量。
局部变量、成员变量、静态变量的核心区别
| 类型 | 声明位置 | 从属于 | 生命周期(作用域) |
|---|---|---|---|
| 局部变量 | 方法或语句块内部 | 方法/语句块 | 从声明处开始,到方法或语句块结束 |
| 成员变量(实例变量) | 类内部,方法外部 | 对象 | 对象创建,成员变量也跟着创建。对象消失,成员变量也跟着消失; |
| 静态变量(类变量) | 类内部,static修饰 | 类 | 类被加载,静态变量就有效; |
包(package)相当于文件夹对于文件的作用。用于管理类、用于解决类的重名问题。
package的使用有两个要点:
注意事项
表 JDK中的主要包
| Java中的常用包 | 说明 |
|---|---|
| java.lang | 包含一些Java语言的核心类,如String、Math、Integer、System和Thread。 |
| java.awt | 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。 |
| java.net | 包含执行与网络相关的操作的类。 |
| java.io | 包含能提供多种输入/输出功能的类。 |
| java.util | 包含一些实用工具类,如定义系统特性、使用与日期日历相关的函数。 |
如果要使用其他包的类,需使用import,从而在本类中直接通过类名来调用,否则就需要书写类的完整包名和类名。
注意要点
- Java会默认导入java.lang包下所有的类,因此这些类我们可以直接使用。
- 如果导入两个同名的类,只能用包名+类名来显示调用相关类:java.util.Date date = new java.util.Date();
静态导入
静态导入(static import): 其作用是用于导入指定类的静态属性和静态方法,这样我们可以直接使用静态属性和静态方法。
【示例】静态导入的使用
package com.itbaizhan;
import static java.lang.Math.*;//导入Math类的所有静态属性
import static java.lang.Math.PI;//导入Math类的PI属性
public class Test2{
public static void main(String [ ] args){
System.out.println(PI);
System.out.println(random());
}
}
instanceof是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:
【示例】使用instanceof运算符进行类型判断
public class Test{
public static void main(String[ ] args) {
Student s = new Student("高淇",172,"Java");
System.out.println(s instanceof Person);
System.out.println(s instanceof Student);
}
}
两条语句的输出结果都是true
继承使用要点
子类重写父类的方法,可以用自身行为替换父类行为。重写是实现多态的必要条件。
方法重写需要符合下面的三个要点:
= =:方法名、形参列表相同。≤:返回值类型和声明异常类型,子类小于等于父类。≥:访问权限,子类大于等于父类。final关键字的作用:
修饰变量: 被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final int MAX_SPEED = 120;
**修饰方法:**该方法不可被子类重写。但是可以被重载!
final void study(){}
修饰类: 修饰的类不能被继承。比如:Math、String等。
final class A {}

final修饰类如图所示。

核心是“将父类对象作为子类的属性”。
组合比较灵活。继承只能有一个父类,但是组合可以有多个属性。所以,有人声称“组合优于继承,开发中可以不用继承”,但是,不建议大家走极端。
对于is -a关系建议使用继承,has-a关系建议使用组合。
比如:上面的例子,Student is a Person这个逻辑没问题,但是:Student has a Person就有问题了。这时候,显然继承关系比较合适。
再比如:笔记本和芯片的关系显然是has-a关系,使用组合更好。
本节作业
所有类都是Object类的子类,也都具备Object类的所有特性。
Object类基本特性

Object类中定义有public String toString()方法,其返回值是 String 类型。Object类中toString方法的源码为:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
根据如上源码得知,默认会返回“类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。
【示例】重写toString()方法
class Person {
String name;
int age;
@Override
public String toString() {
return name+",年龄:"+age;
}
}
public class Test {
public static void main(String[ ] args) {
Person p=new Person();
p.age=20;
p.name="李东";
System.out.println("info:"+p);
Test t = new Test();
System.out.println(t);
}
}
执行结果如图所示:

IDEA快捷键和相关操作:
类的结构视图:alt+7
看类的源码:ctrl+左键
自动生成构造器、get、set方法、equals等:alt+insert
查看错误:alt+enter
快捷输出常见字符串:
a) main public static void main(String[] args){}
b) sout System.out.println();
c) soutm System.out.println(“描述:所在类中的,所在方法”);
==代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象。
equals()提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。
equals()默认是比较两个对象的hashcode。但,可以根据自己的要求重写equals方法。
重写的话,快捷键里面,ctrl+return

【示例】super关键字的使用
public class TestSuper01 {
public static void main(String[ ] args) {
new ChildClass().f();
}
}
class FatherClass {
public int value;
public void f(){
value = 100;
System.out.println ("FatherClass.value="+value);
}
}
class ChildClass extends FatherClass {
public int value;
public int age;
public void f() {
super.f(); //调用父类的普通方法
value = 200;
System.out.println("ChildClass.value="+value);
System.out.println(value);
System.out.println(super.value); //调用父类的成员变量
}
public void f2() {
System.out.println(age); }
}
执行结果如图所示:


属性/方法查找顺序:(比如:查找变量h)
构造方法调用顺序:
构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到Object,然后再依次向下执行类的初始化块和构造方法,直到当前子类为止。
**注:**静态初始化块调用顺序,与构造方法调用顺序一样,不再重复。
【示例】继承条件下构造方法的执行过程
public class TestSuper02 {
public static void main(String[ ] args) {
System.out.println("开始创建一个ChildClass对象......");
new ChildClass();
}
}
class FatherClass {
public FatherClass() {
System.out.println("创建FatherClass");
}
}
class ChildClass extends FatherClass {
public ChildClass() {
System.out.println("创建ChildClass");
}
}
执行结果如图所示:

先静态初始化,创建类之后,在堆中给新对象一个空间,在栈中使用栈帧调用构造器的方法,从而给堆中的对象进行初始化,然后将该对象地址传递给对象名字。这样就创建了一个对象。
因此,静态初始化仅一次,静态代码块仅在编译的时候执行一次,不会在每一个对象创建的时候都执行。
import java.util.Objects;
public class Point {
double x;
double y;
static {
System.out.println("Point 静态加载");
}
public Point(double x) {
System.out.println("POINT的构造器" + x);
}
public static void main(String[] args) {
new X(2);
new X(3);
new X(4);
}
}
class X extends Point {
static {
System.out.println("X的静态代码块");
}
public X(double x) {
super(x);
System.out.println("POINT子类构造器" + x);
}
}
执行结果
Point 静态加载
X的静态代码块
POINT的构造器2.0
POINT子类构造器2.0
POINT的构造器3.0
POINT子类构造器3.0
POINT的构造器4.0
POINT子类构造器4.0
进程已结束,退出代码0
Java是使用访问控制符来控制哪些细节需要封装,哪些细节需要暴露的。
Java中4种访问控制符分别为private、default、protected、public。
访问权限修饰符
| 修饰符 | 同一个类 | 同一个包中 | 子类 | 所有类 |
|---|---|---|---|---|
| private | ☆ | |||
| default | ☆ | ☆ | ||
| protected | ☆ | ☆ | ☆ | |
| public | ☆ | ☆ | ☆ | ☆ |
public:全局友好
protected:父子友好 包内友好
缺省:包内友好
private:类内友好
☆
【注】关于protected的两个细节:
- 若父类和子类在同一个包中,子类可访问父类的protected成员,也可访问父类对象的protected成员。
- 若子类和父类不在同一个包中,子类可访问父类的protected成员,不能访问父类对象的protected成员。
开发中封装的简单规则:
属性一般使用private访问权限。
属性私有后, 提供相应的get/set方法来访问相关属性,这些方法通常是public修饰的,以提供对属性的赋值与读取操作(注意:boolean变量的get方法是is开头!)。
方法:一些只用于本类的辅助性方法可以用private修饰,希望其他类调用的方法用public修饰。
**多态指的是同一个方法调用,由于对象不同可能会有不同的行为。**现实生活中,同一个方法,具体实现会完全不同。 比如:同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。
多态的要点:
父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了
但是这里会出现问题就是,编译器只认你这个Animal的身份,只能用Animal及以上的方法,而不能用Animal的子类的DOG的独有的seeDoor()的方法。。。。即使你的内核是只狗
虽然只能使用Animal及以上的方法,但是在Animal中的方法如果被DOG重写了的话,执行的是DOG里面的算法而不是Animal里面的。

抽象方法
· 抽象类
抽象类的使用要点:
接口就是一组规范(就像我们人间的法律一样),所有实现类都要遵守。
面向对象的精髓,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。
接口的作用
· 为什么需要接口?接口和抽象类的区别?
接口就是比“抽象类”还“抽象”的“抽象类”,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。
接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。
**接口和实现类不是父子关系,是实现规则的关系。**比如:我定义一个接口Runnable,Car实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现Runnable接口。、
定义接口的详细说明:
**访问修饰符:**只能是public或默认。
**接口名:**和类名采用相同命名机制。
**extends:**接口可以多继承。
**常量:**接口中的属性只能是常量,总是:public static final 修饰。不写也是。
**方法:**接口中的方法只能是:public abstract。 省略的话,也是public abstract。
子类通过implements来实现接口中的规范。
接口不能创建实例,但是可用于声明引用变量类型。
一个类实现了接口,必须实现接口中所有的方法,并且这些方法只能是public的。
JDK1.8(不含8)之前,接口中只能包含静态常量、抽象方法,不能有普通属性、构造方法、普通方法。
JDK1.8(含8)后,接口中可以包含普通的静态方法、默认方法。
JAVA8之前,接口里的方法要求全部是抽象方法。
JAVA8(含8)之后,以后允许在接口里定义默认方法和静态方法。
JDK8新特性_默认方法
Java 8及以上新版本,允许给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可,这个特征又叫做默认方法(也称为扩展方法)。
默认方法和抽象方法的区别是抽象方法必须要被实现,默认方法不是。作为替代方式,接口可以提供默认方法的实现,所有这个接口的实现类都可以得到默认方法。
张怡桢说:
但是若哪个子类重写了默认,使用接口引用调用该默认方法时,真正运行的代码是子类里面的默认方法。
就是说,编译类型:接口;运行类型:子类
方法有无认编译,运行有无先看子类再往上
public class Test {
public static void main(String[] args) {
A a = new Test_A();
a.moren();
}
}
interface A {
default void moren(){
System.out.println("我是接口A中的默认方法!");
}
}
class Test_A implements A {
@Override
public void moren() {
System.out.println("Test_A.moren");
}
}
JDK8新特性_静态方法
JAVA8以后,我们也可以在接口中直接定义静态方法的实现。
这个静态方法直接从属于接口(接口也是类,一种特殊的类),可以通过接口名调用。
如果子类中定义了相同名字的静态方法,**那就是完全不同的方法了,直接从属于子类。**可以通过子类名直接调用。
静态方法不存在重写的问题。just it self
接口支持多继承。和类的继承类似,子接口extends父接口,会获得父接口中的一切。
【示例】接口的多继承
interface A {
void testa();
}
interface B {
void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
void testc();
}
public class Test implements C {
public void testc() {
}
public void testa() {
}
public void testb() {
}
}
String是最常用的类,要掌握String类常见的方法,它底层实现也需要掌握好,不然在工作开发中很容易犯错。

Java内存分析中,我们会经常听到关于“常量池”的描述,实际上常量池也分了以下三种:
1. 全局字符串常量池
2. class文件常量池
3. 运行时常量池(Runtime Constant Pool)
我们只关注运行时常量池即可。
【示例】字符串相等判断(以后一般判断字符串值是否相等,使用equals())
String g1 = "北京尚学堂";
String g2 = "北京尚学堂";
String g3 = new String("北京尚学堂");
System.out.println(g1 == g2); // true
System.out.println(g1 == g3); // false
System.out.println(g1.equals(g3)); //true
我们把一个类放在另一个类的内部定义,称为内部类(inner class)。

内部类的两个要点:
注意
内部类只是一个编译时概念,一旦我们编译成功,就会成为完全不同的两个类。对于一个名为Outer的外部类和其内部定义的名为Inner的内部类。编译完成后会出现Outer.class和Outer$Inner.class两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类的相同。
【示例】内部类的定义和使用
/**外部类Outer*/
class Outer {
private int age = 10;
public void show(){
System.out.println(age);//10
}
/**内部类Inner*/
public class Inner {
//内部类中可以声明与外部类同名的属性与方法
private int age = 20;
public void show(){
System.out.println(age);//20
}
}
}

非静态内部类
非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)
【示例】内部类中访问成员变量
/**外部类Outer1*/
class Outer1 {
private int age = 10;
public void show(){
System.out.println(age);//10
}
/**内部类Inner*/
public class Inner1 {
//内部类中可以声明与外部类同名的属性与方法
private int age = 20;
public void show(){
System.out.println(age);//20
System.out.println(Outer1.this.age); //10 访问外部类的普通属性
}
}
}
内部类的访问:
\1. 外部类中定义内部类:new Inner()。
\2. 外部类以外的地方使用非静态内部类:
Outer.Inner varname = new Outer().new Inner()。
静态内部类
定义方式:
static class ClassName {
//类体
}
使用要点:
【示例】静态内部类的访问
/*
测试静态内部类
*/
class Outer2{
private int a = 10;
private static int b = 20;
//相当于外部类的一个静态成员
static class Inner2{
public void test(){
// System.out.println(a); //静态内部类不能访问外部类的普通属性
System.out.println(b); //静态内部类可以访问外部类的静态属性
}
}
}
public class TestStaticInnerClass {
public static void main(String[ ] args) {
//通过 new 外部类名.内部类名() 来创建内部类对象
Outer2.Inner2 inner =new Outer2.Inner2();
inner.test();
}
}
Outer2.Inner2 inner =new Outer2.Inner2();
匿名内部类
适合那种只需要使用一次的类。比如:键盘监听操作等等。在安卓开发、awt、swing开发中常见。
/**
* 测试匿名内部类
*/
public class TestAnonymousInnerClass {
public void test1(A a) {
a.run();
}
public static void main(String[] args) {
TestAnonymousInnerClass tac = new TestAnonymousInnerClass();
tac.test1(new A() {
@Override
public void run() {
System.out.println("匿名内部类测试! 我是新定义的第一个匿名内部类!");
}
});
tac.test1(new A() {
@Override
public void run() {
System.out.println("我是新定义的第二个匿名内部类");
}
});
}
}
interface A {
void run();
}
注意
- 匿名内部类没有访问修饰符。
- 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。
局部内部类
定义在方法内部的,作用域只限于本方法,称为局部内部类。
局部内部类在实际开发中应用很少。
/**
* 测试局部内部类
*/
public class TestLocalInnerClass {
public void show() {
//作用域仅限于该方法
class Inner3 {
public void fun() {
System.out.println("helloworld");
}
}
new Inner3().fun();
}
public static void main(String[ ] args) {
new TestLocalInnerClass().show();
}
}

数组是相同类型数据的有序集合。其中,每一个数据称作一个元素,每个元素可以通过一个索引(下标)来访问它们。数组的四个基本特点:
注意事项
数组有两种:
基本类型数组与引用类型数组
基本类型数组

引用类型数组

数组的初始化方式总共有三种:静态初始化、动态初始化、默认初始化。
1. 静态初始化
除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。
【示例】数组的静态初始化
int [ ] a = { 1, 2, 3 };// 静态初始化基本类型数组;
Man[ ] mans = { new Man(1, 1), new Man(2, 2) };// 静态初始化引用类型数组;
2.动态初始化
数组定义与为数组元素分配空间并赋值的操作分开进行。
【示例】数组的动态初始化
int[ ] a1 = new int[2];//动态初始化数组,先分配空间;
a1[0]=1;//给数组元素赋值;
a1[1]=2;//给数组元素赋值;
3.数组的默认初始化
数组是对象,它的元素相当于对象的属性;每个元素也按照属性的方式被默认初始化。
【示例】数组的默认初始化
int a2[ ] = new int[2]; // 默认值:0,0
boolean[ ] b = new boolean[2]; // 默认值:false,false
String[ ] s = new String[2]; // 默认值:null, null
注:实质上,后面学容器的扩容就是“数组的拷贝”。
数组的遍历
数组元素下标的合法区间:[0, length-1]。我们可以通过下标来遍历数组中的元素,遍历时可以读取元素的值或者修改元素的值。
参数:数组.length
for-each循环
for-each专门用于读取数组或容器中所有的元素。
注意事项
for-each增强for循环在遍历数组过程中不能修改数组中某元素的值.for-each仅适用于遍历,不涉及有关索引(下标)的操作。System.arraycopy(object src,int srcpos,object dest, int destpos,int length)
该方法可以将src数组里的元素值赋给dest数组的元素,其中srcpos指定从src数组的第几个元素开始赋值,length参数指定 将src数组的多少个元素赋给dest数组的元素。

Arrays类包含了:排序、查找、填充、打印内容等常见的数组操作。
import java.util.Arrays;
public class Test {
public static void main(String args[ ]) {
int[ ] a = { 1, 2 };
System.out.println(a); // 打印数组引用的值;
System.out.println(Arrays.toString(a)); // 打印数组元素的值;
}
}
菜鸟雷区
此处的Arrays.toString()方法是Arrays类的静态方法,不是前面讲的Object的toString()方法。
【示例】使用Arrays类实现二分法查找法
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
int[ ] a = {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.sort(a); //使用二分法查找,必须先对数组进行排序;
System.out.println(Arrays.toString(a));
//返回排序后新的索引位置,若未找到返回负数。
System.out.println("该元素的索引:"+Arrays.binarySearch(a, 12));
}
}
示例】使用Arrays类对数组进行填充
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
int[ ] a= {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.fill(a, 2, 4, 100); //将2到4索引的元素替换为100;
System.out.println(Arrays.toString(a));
}
}
//动态初始化
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
int[ ][ ] a = new int[3][ ];
// a[0] = {1,2,5}; //错误,没有声明类型就初始化
a[0] = new int[ ] { 1, 2 };
a[1] = new int[ ] { 2, 2 };
a[2] = new int[ ] { 2, 2, 3, 4 };
System.out.println(a[2][3]);
System.out.println(Arrays.toString(a[0]));
System.out.println(Arrays.toString(a[1]));
System.out.println(Arrays.toString(a[2]));
}
}
//静态初始化
public class Test {
public static void main(String[ ] args) {
int[ ][ ] a = { { 1, 2, 3 }, { 3, 4 }, { 3, 5, 6, 7 } };
System.out.println(a[2][3]);
}
}
雇员表
| ID | 姓名 | 年龄 | 职能 | 入职日期 |
|---|---|---|---|---|
| 1001 | 高淇 | 18 | 讲师 | 2-14 |
| 1002 | 高小七 | 19 | 助教 | 10-10 |
| 1003 | 高小八 | 20 | 班主任 | 5-5 |
使用二维数组保存表格数据
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
Object[ ] a1 = {1001,"高淇",18,"讲师","2-14"};
Object[ ] a2 = {1002,"高小七",19,"助教","10-10"};
Object[ ] a3 = {1003,"高小琴",20,"班主任","5-5"};
Object[ ][ ] emps = new Object[3][ ];
emps[0] = a1;
emps[1] = a2;
emps[2] = a3;
System.out.println(Arrays.toString(emps[0]));
System.out.println(Arrays.toString(emps[1]));
System.out.println(Arrays.toString(emps[2]));
}
}
注意事项
- 此处基本数据类型
1001,本质不是Object对象。JAVA编译器会自动把基本数据类型“自动装箱”成包装类对象。大家在下一章学了包装类后就懂了。
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
Emp[] emps = {
new Emp(1001,"高淇",18,"讲师","2-14"),
new Emp(1002,"高小七",19,"助教","10-10"),
new Emp(1003,"高小八",20,"班主任","5-5")
};
for (Emp e:emps){
System.out.println(e);
}
}
}
class Emp {
private int id;
private String name;
private int age;
private String job;
private String hiredate;
public Emp(int id, String name, int age, String job, String hiredate) {
this.id = id;
this.name = name;
this.age = age;
this.job = job;
this.hiredate = hiredate;
}
@Override
public String toString() {
return "["+id+","+name+","+age+","+job+","+hiredate+"]";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public String getHiredate() {
return hiredate;
}
public void setHiredate(String hiredate) {
this.hiredate = hiredate;
}
}
多个对象做比较,就要有“比较规则”,然后实现排序。
事实上,java中排序算法的底层也依赖Comparable接口。
Comparable接口中只有一个方法:
public int compareTo(Object obj) obj为要比较的对象
方法中,将当前对象和obj这个对象进行比较,如果大于返回1,等于返回0,小于返回-1. (此处的1也可以是正整数,-1也可以是负整数)。 compareTo方法的代码也比较固定:
Arrays的sort底层依靠compareto返回值来进行排序
小了返回-1,等于返回0,大了返回1:升序
小了返回1,等于返回0,大了返回-1:降序
重写Man的toString,输出Arrays.toString(mans)即可输出排了序的mans
使用Arrays类对数组元素进行排序
import java.util.Arrays;
public class Test {
public static void main(String[ ] args) {
Man[ ] msMans = { new Man(3, "a"), new Man(60, "b"), new Man(2, "c") };
Arrays.sort(msMans);
System.out.println(Arrays.toString(msMans));
}
}
class Man implements Comparable {
int age;
int id;
String name;
public Man(int age, String name) {
super();
this.age = age;
this.name = name;
}
public String toString() {
return this.name;
}
public int compareTo(Object o) {
Man man = (Man) o;
if (this.age < man.age) {
return -1;
}
if (this.age > man.age) {
return 1;
}
return 0;
}
}