Java基础知识
1 Java开发环境1.1Java语言特点简单性高性能编译性 解释性面向对象分布式处理健壮性安全性开源 跨平台什么是跨平台性?通过Java语言编写的应用程序在不同的系统平台上都可以运行。原理是什么?只要在需要运行java应用程序的操作系统上,先安装一个Java虚拟机(JVM Java Virtual Machine)即可。由JVM来负责Java程序在该系统中的运行。因为有了JVM,所以同一个Jav
1 Java开发环境
1.1 Java语言特点
简单性 高性能
编译性 解释性
面向对象 分布式处理
健壮性 安全性
开源 跨平台
什么是跨平台性?
通过Java语言编写的应用程序在不同的系统平台上都可以运行。
原理是什么?
只要在需要运行java应用程序的操作系统上,先安装一个Java虚拟机(JVM Java Virtual Machine)即可。由JVM来负责Java程序在该系统中的运行。
因为有了JVM,所以同一个Java程序在三个不同的操作系统中都可以执行。这样就实现了Java程序的跨平台性。也称为Java具有良好的可移植性。
1.2 JDK
JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心。
它包含了:
1、 JAVA开发工具(jdk\bin)
2、 基础开发库(jdk\jre\lib\rt.jar)
3、 基础开发库的源码(jdk\src.zip)
下载JDK:http://www.oracle.com
配置环境变量
JAVA_HOME
该配置是要配置jdk的安装目录,来明确要使用哪个版本的jdk。
例如:我把jdk安装在了D:\Java\jdk7。配置如下:
PATH
系统会在path配置的值中,寻找可执行文件。
需要把jdk\bin添加到path中。
JDK JRE JVM的关系
1、JDK–Java Development Kit是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具和Java基础类库。
2、JRE–Java Runtime Environment是运行JAVA的运行时环境,包含JVM和Java核心类库。
3、JVM–Java Virtual Machine,Java虚拟机的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
1.3 运行与工作原理
1、 编译:通过javac命令将java的源文件也就是.java文件编译成.class文件。
2、 执行:
先启动java虚拟机,不同的操作系统有不同的jvm,因此java跨平台。
再通过JVM加载HelloWorld.class字节码文件。并加载main方法执行程序。
Java的”一次编写,处处运行”是如何实现的?
Java程序会被编译成字节码组成的class文件,这些字节码可以运行在任何平台,因此Java是平台独立的。
2 数据类型+运算符
2.1 标识符
可以简单的理解为一个名字。在Java中,我们需要标识代码的很多元素,包括类名、方法、字段、变量、包名等。我们选择的名称就称为标识符,并且遵循以下规则:
A. 标识符可以由字母、数字、下划线(_)、美元符($)组成,但不能包含 @、%、空格等其它特殊字符。
B. 不能以数字开头。
C. 标识符是严格区分大小写的。
D. 标识符的命名最好能反映出其作用,做到见名知意。
2.2 关键字
在java语言中已经被赋予特定意义的一些单词。一共有53个关键字。其中有两个保留字:const和goto。关键字不能被用作标识符!!
2.3 注释
Java代码的解释说明。不影响程序的运行,用于辅助读程。
//单行注释
/* 多行注释 /
/* 文档注释 */
2.4 变量
在JAVA中,有的数据值是不固定的,总在变,我们还需要记录这些值,我们可以把这些值理解为变量。
我们通过三个元素来描述变量:变量类型,变量名以及变量值。
int age=18; //声明int类型的变量并赋值
String tel; //声明String类型的变量
注意:
1、 变量名必须是一个有效的标识符。
2、 变量名不可以使用java关键字
3、 变量名不能重复
2.5 常量
在程序运行过程中一致不会改变的量成为常量。
2.6 数据类型
1 基本类型(八种)
位bit 字节byte 1byte=8bit
2 引用类型
引用类型是一个对象类型,值是什么呢?它的值是指向内存空间的引用,就是地址,所指向的内存中保存着变量所表示的一个值或一组值。如:类,接口,数组,后面讲。。
3 基本类型的字面值(5条)
3.1 整数字面值是int类型
int a = 999999999;//错,右侧是int类型,但是超出范围
3.2 byte,short,char三种比int小的整数可以用范围内的值直接赋值
byte b=127;//对
byte b=128;//错,右面已经超过byte范围是int类型的数据
3.3 浮点数的字面值是double类型
double a=3.14;//对
float a=3.14;//错,右面是double,float是四字节double是八字节存不下
3.4 字面值后缀l f d
L –long 如:long a = 99999999999;//错,超出范围,解决方案加L
F –float 如:float a = 3.14;//错,右面是double类型,解决方案加F
D –double 如:double a=3;//错,右面是int,解决方案加D或者改成3.0
3.5 进制前缀
0x - 16进制
0 -8进制
\u -char类型,16进制
4 基本类型的类型转换
4.1 小到大(隐式转换)
Byte a =120;
Int b=a;//直接转
4.2 大到小(显式转换)
需要强制类型转换
int xx = 356;
byte y=(byte) xx;
注意:小数转成整数,小数直接舍弃
5 运算规则(5条)
5.1 计算结果的数据类型,与最大类型一致
3/2 得1 ,而不是1.5,结果是int类型
3d/2 得1.5,相当于double/int,结果是double类型
5.2 byte,short,char三种比int小的整数,运算时会先自动转换成int
byte a=3;
byte b=4;
byte c=a+b;//错,运行时,byte会先自动转成int再运算,int+int还是int
5.3 整数运算溢出
整数运算,类似于一个中标,转到最大时,再转会回到最小。
计算:光速运行一年的长度是多少米
System.out.println(300000000606024365);
System.out.println(300000000l606024365);
5.4 浮点数运算不精确
java提供了解决方案,后面就会讲到。
System.out.println(1-0.8);
System.out.println(4.35*100);
5.5 浮点数的特殊值
Infinity 无穷大 3.14/0
Nan not a number 0/0.0
5.6 char 型变量中能不能存储一个中文汉字,为什么?
char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。
3 方法+流程控制+循环
3.1 运算符
3.2 分支结构1:if
顺序结构的程序虽然能解决计算、输出等问题,但不能做判断再选择。对于要先做判断再选择的问题就要使用分支结构。
单分支:
if(判断条件){
代码。。。
}
多分支:
if(判断条件){
代码1。。。
}else{
代码2。。。
}
嵌套分支:
if(判断条件1){
代码1。。。
}else if(条件2){
代码2。。。
} else if(判断条件3){
代码3。。。
}else{
代码4。。。
}
3.3 [了解]分支结构2:switch
当一个case成立,从这个case向后穿透所有case,包括default,直到程序结束或者遇到break程序才结束。
switch(expr1)中,expr1是一个整数表达式, 整数表达式可以是int基本类型或Integer包装类型,由于byte,short,char都可以隐含转换为int,所以也支持。
注意: jdk1.7以后新增 String
switch(变量或者表达式){
case 1:
case 2:
case 3:
case 4:
default:
}
3.4 循环结构1:for
循环结构是指在程序中需要反复执行某个功能而设置的一种程序结构。
它由循环体中的条件,判断继续执行某个功能还是退出循环。
根据判断条件,循环结构又可细分为先判断后执行的循环结构和先执行后判断的循环结构。
for(开始条件;循环条件;更改条件){
循环体代码…
}
3.5 拓展
3.5.1 &和&&的区别
当一个&表达式在求值的时候,两个操作数都会被求值,&&更像 是一个操作符的快捷方式。当一个&&表达式求值的时候,先计算第一个操作数,如果它返回true才会计算第二个操作数。如果第一个操作数 取值为fale,第二个操作数就不会被求值
3.5.2 a=a+4和a+=4的区别
byte a = 1;
// a=(byte) (a+4);//右侧int,左侧byte,大转小,强转。
// a=(byte) (a+4);//右侧int,左侧byte,大转小,强转。
a+=4;//会自动完成数据类型的转换
//注意:a=a+4和a+=4是有区别的!!
System.out.println(a);
4 循环+数组
4.1 嵌套for循环
4.1.1 概述
根据外层的条件,判断里层能否执行,如果能执行,就把里层代码都循环完毕后,再继续执行外层,继续判断。。
4.1.2 形式
for(…){
for(…){
}
}
4.2 break和continue
用来终止循环,可以用两种方式
4.2.1 形式
break: 中断当前循环,简单粗暴
for(){
代码1
if(条件){
代码3…
break;//如果成立,直接跳出这个for循环
}
代码2…
}
continue:跳出本次循环,进入下一轮
for(){
代码1
if(条件){
代码3…
continue;//如果成立,跳出本次for循环,进入下一轮
}
代码2…
}
4.3 循环结构2:while
先判断,再执行
while(执行条件){
代码…
}
4.3 循环结构2:while
先判断,再执行
4.4 循环结构3:do-while
先执行,再判断
do{
代码…
}while(执行条件);
4.5 变量
4.5.1 概念
可以改变的数,称为变量。一般通过三部分来描述一个变量。变量类型,变量名,变量值。其中三部分都是可以改变的,根据需要来确定即可。
变量的使用原则:就近原则。尽量控制到最小范围。。
4.5.2 局部变量
定义在方法里,或者局部代码块中。
注意:必须手动初始化,来分配内存。如:int i=5;
作用域也就是方法里或者局部代码块里,方法运行完内存就释放了。
4.5.3 成员变量
定义在类里。
注意:不用初始化,也会自动被初始化成默认值。
作用域是整个类中,类消失了,变量才释放。
4.6 拓展:
三种循环的区别
三种循环都可以互相代替
1、 for:知道循环次数
2、 while/do while:当循环次数不确定时
3、 while:先判断,不符合规则,不执行代码
4、 do while:代码最少被执行一次,再去判断,符合规则,再次执行代码
5 数组+变量
5.1 方法
5.1.1 概述
被命名的代码块,方法可以含参数可以不含参数
可以提高代码的复用性
5.1.2 形式
修饰符 返回值 方法名(【参数】){
方法体;
}
5.1.3 练习1:方法调用
package day999;
public class tt {
public static void main(String[] args) {
System.out.println(1);
int num=f3(5);
System.out.println(num);
String str = f4("学习使我快乐");
System.out.println(str);
System.out.println(2);
}
public static String f4(String desc){
return desc+",我信你个鬼";
}
public static int f3(int i){
return i*10;
}
}
5.2 方法的重载
5.2.1 概念
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数列表(也就是说参数的个数和类型不同)。
程序调用方法时,可以通过传递给它们的不同个数和类型的参数来决定具体使用哪个方法。
5.3 数组
5.3.1 概念
数组Array是用于储存多个相同类型数据的集合。
想要获取数组中的元素值,可以通过元素的下标来获取,下标是从0开始的。
5.3.2 创建数组
一般分为动态初始化和静态初始化
动态初始化:int[] a = new int[5];
1 新建int[],长度是5
2刚创建好的数组都是默认值0,int类型的数据默认值是0
3把数组的地址值给变量a保存
静态初始化1:int[] a ={1,2,3,4,5,6,7,8};
静态初始化2:int[] a =new int[]{1,2,3,4,5};
5.3.3 数组的长度
1 length属性获取数组长度
2数组一旦创建,长度不可变3
3 允许0长度的数组
5.4 数组的遍历
从头到尾,依次访问数组的位置。
for(int i=0;i<a.length;i++){
syso(a[i]);
}
5.5 数组工具类Arrays
5.5.1 Arrays.toString(数组)
把数组里的数据,用逗号连接成一个字符串。
格式:[10, 14, 20, 46, 51]
5.5.2 Arrays.sort(数组)
对数组排序,对于基本类型的数组使用优化后的快速排序算法,效率高。
对引用类型数组,使用优化后的合并排序算法。
5.5.3 Arrays.copyOf(数组,新的长度)
把数组复制成一个指定长度的新数组。
新数组长度大于原数组,相当于复制,并增加位置。–数组的扩容
新数组长度小于原数组,相当于截取前一部分数据。–数组的缩容
5.6 拓展:
5.6.1 了解二维数组
存放数组的数组,也就是说数组里存的还是数组的数据形式
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
5.6.2 了解冒泡排序
相邻位置比较,从小到大排序,如果小就往前换。i代表从头到尾遍历循环数据。
package day0000;
import java.util.Arrays;
import java.util.Random;
public class TT {
public static void main(String[] args) {
int[] arr = new int[]{43, 36, 45, 18, 24,9,20,32};
int[] arrnew = f1(arr);
System.out.println(Arrays.toString(arrnew));
}
private static int[] f1(int[] a) {
//外循环控制循环次数,如果5个数字,循环4次就行
for (int i = 0; i < a.length-1; i++) {
//内循环控制比大小,循环次数和外循环一样
for (int j = 0; j < a.length-1; j++) {
if(a[j]>a[j+1]){
int t = a[j];
a[j]=a[j+1];
a[j+1]=t;
}
}
}
return a;
}
6 面向对象1
6.1 面向对象
6.1.1 概念
所谓的面向对象是一种编程思想,通过这种思想可以把生活中的复杂事情变得简单化,从原来的执行者变成了指挥者,面向对象是基于面向过程而言的。
我们经常说的面向对象的编程实现(OOP,Object Oriented Programming)
l 面向过程强调的是过程,例如:
1、打开冰箱 2、把大象放进去 3、关上冰箱
l 面向对象强调结果,例如:
1、 饿了,去平台点餐,这个动作就是面向对象。你没有去市场买菜洗菜做饭。。。只要有app就可以了。
6.1.2 三大特征
1、 封装性,把相关的数据封装成一个“类”组件
2、 继承性,是子类自动共享父类属性和方法,这是类之间的一种关系
3、 多态,增强软件的灵活性和重用性
6.2 类和对象
6.2.1 类
1、 Java语言最基本单位就是类,类似于类型。
2、 类是一类事物的抽象。
3、 可以理解为模板或者设计图纸。
6.2.2 对象
每个对象具有三个特点:对象的状态,对象的行为和对象的标识。
1、 对象的状态用来描述对象的基本特征。
2、 对象的行为用来描述对象的功能。
3、 对象的标识是指对象在内存中都有一个唯一的地址用来和其他对象区分开来。
4、 类是一类事物的抽象,对象是具体的实现。
6.2.3 类和对象的关系
1、 计算机语言是用来描述现实世界事物的。属性+行为
2、 那怎么通过java语言描述呢?通过类来描述事物,把事物的属性当做成员变量,把行为当做成员方法。
分析手机事物:
属性:颜色,尺寸,品牌,价格。。。
行为:打电话,发短信,听音乐。。。。
类:手机类,抽取相同的属性和行为
对象:可以按照模板生产很多个手机,比如1号手机对象,包含特有的成员变量和成员方法
6.3 类和对象的创建和使用
6.3.1 练习1:类的创建使用
通过class关键字创建类,通过new关键字创建对象。
package day000000;
public class Test1 {
public static void main(String[] args) {
//p是引用对象,持有了对于Person对象的地址值的引用
//此时的p,含有属性,但都是默认值
Person p = new Person();
//设置属性值
p.name="lisi";
p.age=20;
//调用方法
p.eat();
p.sleep();
}
}
class Person{
//属性--成员变量
String name;
int age;
//行为--方法
void eat(){
System.out.println("吃饭饭");
}
void sleep(){
System.out.println("睡觉觉");
}
}
6.3.2 对象在内存中的存储
Java把内存分成5大区域,我们重点关注栈和堆。
1、 一般来讲局部变量存在栈中,方法执行完毕内存就被释放
2、 对象(new出来的东西)存在堆中,对象不再被使用时,内存才会被释放
3、 每个堆内存的元素都有地址值
4、 对象中的属性都是有默认值的
6.3.3 单一对象内存图
1、 在栈内存中,创建一个引用变量p,持有对象的地址值
2、 在堆内存中,创建Person对象,并且开辟变量的空间,完成初始化
3、 给堆内存中的元素,分配一个唯一标志,地址值。交给p去保存。
4、 p.name=”lisi”;p.age=20;就去堆内存中找唯一的地址值,找到Person对象,并对其属性进行修改赋值。
5、 p.eat();就去堆内存中找唯一的地址值,找到Person对象,执行Person对象的方法。
6.3.4 多对象内存图
1、 变量p和变量p1不是一片空间,p1需要开辟新的空间
2、 Person p1=new Person,这时只要有new,就会新开辟空间在堆内存中存入对象。
6.4 封装
6.4.1 概述
封装是指隐藏对象的属性和实现细节,仅仅对外提供公共的访问方式。
好处:
1、 提高安全性
2、 提高重用性
案例:
1、 类
2、 方法
6.4.2 private关键字
是一个权限修饰符,用于修饰成员变量和成员函数,被私有化的成员只能在本类中访问。
想要修改只能,对外提供公共的,get和set方法。
6.5 构造方法
加粗样式1.5.1 概念
构造方法是一种特殊的方法,它是一个与类同名且返回值类型为同名类类型的方法。对象的创建就是通过构造方法来完成,其功能主要是完成对象的创建或者对象的初始化。当类实例化一个对象时会自动调用构造方法。
构造方法和其他方法一样也可以重载。
6.5.2 形式
可以无参也可以有参
修饰符 类名(【参数】){
代码……
}
6.6 拓展
6.6.1 创建对象的流程
Person p = new Person();//短短这行代码发生了很多事情
-
把Person.class文件加载进内存
-
在栈内存中,开辟空间,存放变量p
-
在堆内存中,开辟空间,存放Person对象
-
对成员变量进行默认的初始化
-
对成员变量进行显示初始化
-
执行构造方法(如果有构造代码块,就先执行构造代码块再执行构造方法)
-
堆内存完成
-
把堆内存的地址值赋值给变量p ,p就是一个引用变量,引用了Person对象的地址值
6.6.2 匿名对象
没有名字的对象,是对象的简化表示形式。
使用场景:
1、 当被调用的对象只调用一次时(多次会创建多个对象浪费内存)
Demo d = new Demo();
d.sleep();
d.game();
//这个d就是对象的名字。
也可以写成:
new Demo().show();//创建了一个对象调方法
new Demo().game();//又创建了一个对象调方法
7 面向对象2
7.1 构造代码块和局部代码块
7.1.1 构造代码块
1、 在类的内部,方法外部,的代码块。
2、 通常用于抽取构造方法中的共性代码。
3、 每次调用构造方法前都会调用构造代码块
4、 优先于构造方法加载
class c{
String country;
{
country="中国";
}
public c() {
System.out.println("1号选手,来自"+country);
}
public c(int a) {
System.out.println("2号选手,也来自"+country);
}
}
7.1.2 局部代码块
1、 在方法里面的代码块
2、 通常用于控制变量的作用范围,出了括号就失效
3、 变量的范围越小越好,成员变量会有线程安全问题
4、 总结:执行顺序:
构造代码块是最优先的,局部代码块顺序执行
7.2 this关键字
7.2.1 概念
this代表本类对象的一个引用对象。
构造函数中,this()必须放在第一行。
7.2.2 形式
//name=name;
//age=age;
其实是想把Student类的局部变量name的值赋值给成员变量,相当于你想操作是这样的:
//Student.name=name;
但是你不能直接写类名,这时候就用代表本类的对象this来完成。代码变成了:
this.name=name;
7.2.3 练习1:当变量名相同时
当局部变量和成员变量同名时,用于区分。
如果附近有同名变量,会遵从变量的就近原则,那么怎么调用成员变量呢?
package day99999;
public class T {
public static void main(String[] args) {
Animal a = new Animal();
a.run("大黄");
System.out.println(a.name);
}
}
class Animal{
String name;
/*
//当局部变量的名字和成员变量不一样时简单,拿着局部变量的值直接给成员变量赋值就行了
public void run(String n){
name=n;
}*/
//问题来了,当局部变量和成员变量同名时呢??
public void run(String name){
//name=name;//变量的就近使用原则,这样用的都是最近的也就是一直在用局部变量的
this.name=name;//this调用成员变量
}
}
7.3 访问控制符
用来控制一个类,或者类中的成员的访问范围。
7.4 继承
7.4.1 概念
继承是面向对象最显著的一个特性。
继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。
Java继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类/超类/基类。
这种技术使得复用以前的代码非常容易,能够大大缩短开发周期,降低开发费用。
提高复用性:只要继承父类,就能有一样的功能
class A extends c{ //原来的eat()拿走了 }
class B extends c{ //原来的eat()拿走了 }
class c{
public void eat(){
syso("eat");
}
}
7.4.2 特点
1、 使用extends关键字
2、 相当于子类把父类的功能复制了一份
3、 java只支持单继承
4、 继承可以传递(爷爷,儿子,孙子的关系)
5、 不能继承父类的私有成员
6、 继承多用于功能的修改,子类可以拥有父类的功能的同时,进行功能拓展
7、 像是is a 的关系
7.5 super关键字
1、 通过super关键字可以使用父类的内容
2、 super代表父类的一个引用对象
3、 如果用,必须出现在调用位置的第一行
7.6 方法的重写
1、 继承后,子类就拥有了父类的功能
2、 那么在子类中,可以添加子类特有的功能也可以修改父类的原有功能
3、 子类中方法签名与父类完全一样(包括方法的返回值,方法名和参数列表,完全一致)时,会发生覆盖/复写操作,相当于修改功能
注意:
1、父类中的私有方法不能被重写
2、子类重写父类方法时,修饰符要大于等于父类修饰符的权限
7.7 继承中的用法
7.7.1 成员变量的使用
局部的,成员的,父类的。
package day000;
public class Test2_LeiVariable {
public static void main(String[] args) {
Sub2 s= new Sub2();
s.show();
}
}
class Fu2{
int num=20;
}
class Sub2 extends Fu2{
int num;
public void show(){
int num=9;
System.out.println(num);//变量的就近原则
System.out.println(this.num);//调用本类的成员变量,相当于Sub2.num
System.out.println(super.num);//调用父类的成员变量
}
}
7.7.2 成员方法的使用
继承方法,特有方法,重写方法
package day000;
public class Test3_LeiMethod {
public static void main(String[] args) {
Sub3 s=new Sub3();
s.testShow();//调用普通方法,
s.eat();//调用重写方法,子类已经重写了父类的方法,从此再用,就是儿子的实现方式
}
}
class Fu3{
public void show(){
System.out.println("Fu...show()");
}
public void eat(){
System.out.println("爸爸吃肉");
}
}
class Sub3 extends Fu3{
//普通方法
public void testShow(){
super.show();//调用父类的eat()
}
//重写方法
//方法签名完全一致的情况就是发生了重写
public void eat(){
System.out.println("儿子吃肉");
}
}
7.7.3 构造方法的使用
1、 子类创建对象时,默认会去访问父类的无参构造方法
2、 在构造方法的第一行,都有一条默认的语句:super();
3、 父类没有无参构造时,可以用super调用父类的其他构造
package day000;
public class Test4_LeiConstruct {
public static void main(String[] args) {
Sub4 s = new Sub4();//创建子类对象,在调用子类无参构造前,会先去调用父类的无参构造
}
}
class Fu4{
public Fu4(){
System.out.println("Fu4.Fu4()");
}
public Fu4(String name){
System.out.println("Fu.."+name);
}
}
class Sub4 extends Fu4{
public Sub4(){
// super();//默认就存在的,而且在第一行
super("zhangsan");//调用父类中含参的构造方法
System.out.println("Sub4.Sub4()");
}
public Sub4(String name){
this();//调用本类的无参构造
System.out.println("Sub.."+name);
}
}
7.8 拓展
7.8.1 this和super的区别
1、 this代表本类对象的引用,super代表父类对象的引用。
2、 this用于区分局部变量和成员变量
3、 super用于区分本类变量和父类变量
4、 this.成员变量 this.成员方法() this(【参数】)代表调用本类内容
5、 super.成员变量 super.成员方法() super(【参数】),代表调用父类内容
6、 this和super不可以同时出现在同一个构造方法里,他们两个只要出现都得放在第一行,同时出现的话,到底第一行放谁呢。。
7.8.2 重写与重载的区别(Overload和Override的区别)
1、重载:是指同一个类中的多个方法具有相同的名字,但这些方法具有不同的参数列表,即参数的数量或参数类型不能完全相同
2、重写:是存在子父类之间的,子类定义的方法与父类中的方法具有相同的方法名字,相同的参数表和相同的返回类型
3、重写是父类与子类之间多态性的一种表现
4、重载是一类中多态性的一种表现
7.8.3 继承的内存结构
8 面向对象3
8.1 static
8.1.1 概念
1、 是java中的一个关键字
2、 用于修饰成员(成员变量和成员方法)
8.1.2 特点
1、 可以修饰成员变量,成员方法
2、 随着类的加载而加载,优先于对象加载
3、 只加载一次,就会一直存在,不再开辟新空间
4、 全局唯一,全局共享
5、 可以直接被类名调用
6、 静态只能调用静态,非静态可以随意调用
7、 static不能和this或者super共用,因为有static时可能还没有对象
8.1.3 练习1:入门案例
package cc;
public class aa {
public static void main(String[] args) {
//特点2,优先于对象加载,有没有对象都有static
int i = Student.age;
System.out.println(i);
//特点3,只加载一次,就会一直存在,不再开辟新空间
//创建对象
Student s = new Student();
Student s1 = new Student();
//调用方法
System.out.println(s.name);
System.out.println(s.age);
s.speak();
s.eat();
}
}
class Student{
String name="张三";
static int age=20;
public void eat(){
System.out.println("eat..");
}
public static void speak(){
System.out.println("speak...");
}
}
8.1.4 静态方法内存图
8.1.5 练习2:静态调用关系
静态只能调用静态(变量或者方法),非静态可以访问所有的不管是不是静态
package cc;
public class O {
public static void main(String[] args) {
Teacher t = new Teacher();
t.speak();
}
}
class Teacher{
static int age=30;
String name="王康";
//非静态测试
public void speak(){
System.out.println(age);//非静态能调用静态变量
System.out.println(name);//非静态能调用非静态变量
qiao();//非静态能调用静态方法
}
//静态测试
public static void qiao(){
System.out.println(age);//静态能调用静态
// System.out.println(name);//静态不能调用非静态变量,只能调用静态变量
// speak();//静态不能调用非静态方法,只能调用静态方法
}
}
8.2 静态代码块、构造代码块、局部代码块
8.2.1 静态代码块
随着类的加载而加载,并且只被加载一次,一般用于项目的初始化
static{…}
8.2.2 概述
1、 静态代码块:在类加载时就加载,并且只被加载一次,一般用于项目的初始化
2、 构造代码块:在调用构造方法前会自动调用,每次创建对象都会被调用
3、 局部代码块:方法里的代码块,方法被调用时才会执行
4、 静态代码块:static{ },位置:在类里方法外
5、 TODO创建测试类,类中写好静态代码块,构造代码块,构造方法,普通方法里嵌套局部代码块。测试他们的执行顺序。
6、 静态 - 构造代码块 - 构造方法 - 局部
8.2.3 测试
package cn.tedu.staticdemo;
public class Test4 {
public static void main(String[] args) {
Car c = new Car();c.show();
Car c2 = new Car();
}
}
class Car{
//静态代码块
//1,在类第一次加载时就加载到内存中并一直驻存,
//并全局共享,直到类消失静态资源才消失
//2,并且静态资源只被加载一次
static {
System.out.println(1);
}
//构造代码块
{
System.out.println(2);
}
// 构造方法
public Car() {
System.out.println(3);
}
public void show() {
// 局部代码块
{
System.out.println(6);
}
System.out.println(4);
System.out.println(5);
}
}
8.3 final
8.3.1 概念
1、 是java提供的一个关键字
2、 final是最终的意思
3、 final可以修饰类,方法,成员变量
8.3.2 特点
1、 被final修饰的类,不能被继承
2、 被final修饰的方法,不能被重写
3、 被final修饰的变量是个常量,值不能被更改
4、 常量的定义形式: final 数据类型 常量名 = 值
8.3.3 入门案例
package cc;
public class P {
public static void main(String[] args) {
Zi d = new Zi();
d.speak();
System.out.println(d.name);
d.test();
}
}
class Fu{
//final lass Fu{//1,被final的类,不能被继承
final String name="欢欢";
public final void speak(){
System.out.println("Fu..speak()");
}
}
class Zi extends Fu{
/*
//2,被final的方法,不能被重写
* public void speak(){
System.out.println("Fu..speak()");
}*/
public void test(){
// super.name="大黄";//3,被final的变量,不能重新被赋值
System.out.println(super.name);
}
}
8.4 多态
8.4.1 概念
多态指同一个实体同时具有多种形式。它是面向对象程序设计(OOP)的一个重要特征。
主要是指同一个对象,在不同时刻,代表的对象不一样,指的是对象的多种形态。
好处是可以把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,统一调用标准。
例如:水,在不同时刻可以有多种形态,包括水蒸气,冰,水。
Java怎么体现多态呢?狗有两种形态:狗和小动物
class Animal
class Dog extends Animal
Dog d = new Dog();//狗是狗
Animal a=new Dog();//狗是小动物,创建了狗的对象,赋值给动物对象,这就是多态
8.4.2 特点
1、 多态的前提是继承
2、 要有方法的重写
3、 父类引用指向子类对象,如:Animal a = new Dog(); – 小到大,向上转型
4、 多态中,编译看左边,运行看右边
8.4.3 入门案例
package cc;
public class p {
public static void main(String[] args) {
/*Animal a = new Animal();
a.eat();
Dog d = new Dog();
d.kanjia();
d.eat();*/
//父类引用指向子类对象--把右面的小的,给了左面大的。相当于从小到大的过程,向上造型。
Animal an = new Dog();
an.eat();//只可以用父类的功能,这就统一了调用的标准
//狗吃肉,由于eat()发生了重写,所以父类方法被覆盖掉了
}
}
class Animal{
public void eat(){
System.out.println("大小通吃");
}
}
class Dog extends Animal{
public void kanjia(){
System.out.println("狗可以看家");
}
public void eat(){
System.out.println("狗吃肉");
}
}
8.5 多态的好处
1、 多态可以让我们不用关心某个对象到底是什么具体类型,就可以使用该对象的某些方法。
2、 提高了程序的扩展性和可维护性
public class Q {
public static void main(String[] args) {
//多态--父类引用指向子类对象
Dog a2=new Dog();
Cat a2 = new Cat();
Animal2 a2 = new Dog();
eat(a2);//传入的参数满足条件就可以,运行右面的功能
}
//封装共性方法,不关心具体类型,只要传入的参数是子类就可以
public static void eat(Animal2 a){
a.eat();
}
}
8.6 多态的使用
8.6.1 特点
1、成员变量:使用的是父类的
2、成员方法:由于存在重写现象所以使用的是子类的
3、静态成员:随着对象而存在,谁调用的就返回谁的
8.6.2 测试
package cc;
public class K {
public static void main(String[] args) {
Fu2 f = new Zi2();//多态,只能用父类的。。。
//多态的成员变量,就是父类的
System.out.println(f.num);//10
//成员方法,因为存在重写,所以是子类的
f.study();//Zi..study()
//静态成员,谁调用就是谁的
System.out.println(f.count);//10
f.sleep();//Fu..sleep()
}
}
class Fu2{
int num=10;
static int count=10;
public void study(){
System.out.println("Fu..study()");
}
public static void sleep(){
System.out.println("Fu..sleep()");
}
}
class Zi2 extends Fu2{
int num=20;
static int count=20;
public void study(){
System.out.println("Zi..study()");
}
public static void sleep(){
System.out.println("Zi..sleep()");
}
}
8.7 异常
8.7.1 概述
用来封装错误信息的对象。
组成结构:类型,提示,行号。
8.7.2 异常的继承结构
Throwable - 顶级父类
-- Error:系统错误,无法修复
-- Exception:可修复的错误
--RunTimeException
--ClassCastException
--ClassNotFoundException
8.7.3 异常处理
程序中遇到了异常,通常有两种处理方式:捕获或者向上抛出。
当调用了一个抛出异常的方法时,调用位置可以不做处理继续向上抛出也可以捕获异常。
1、捕获方式:
try{
需要捕获的代码
}catch(异常类型 异常名){
处理方案
}
2、抛出方式:
在会发生异常的方法上添加代码:throws 异常类型
例如: public static void main(String[] args) throws Exception{
8.7.4 测试
接收键盘输入的两个数字并做除法运算
package cn.tedu.exception;
import java.util.Scanner;
//测试异常的发生和解决
public class Test6_Exception {
// 接收键盘输入的两个整数并做除法运算
public static void main(String[] args) {
//1,捕获异常:把可能发生异常的代码放在try里,
//当异常发生时会被catch住并执行catch中的代码执行异常处理的代码
try {
int a = new Scanner(System.in).nextInt();
int b = new Scanner(System.in).nextInt();
System.out.println(a/b);
} catch (Exception e) {
//提出解决方案
System.out.println("您输入的两次整数有误!");
}
}
}
8.8 拓展
8.8.1 静态代码块,构造代码块,局部代码块
执行顺序:静态代码块—构造代码块—构造函数
7、 静态代码块:在类加载时就加载,并且只被加载一次,一般用于项目的初始化
8、 构造代码块:在创建对象时会自动调用,每次创建对象都会被调用
9、 局部代码块:方法里的代码块
package cc;
public class haha {
public static void main(String[] args) {
//必须创建对象,不然静态代码块咋随着类的加载而加载。。
Block m = new Block();//静态1 静态2 构造1 构造2 局部1
System.out.println();
Block m2 = new Block();//构造1 构造2 局部1,说明静态代码只被执行一次就是在类第一次初始化时
}
}
class Block{
{
System.out.println("构造1");
}
static{
System.out.println("静态1");
}
public Block(){
{
System.out.println("局部1");
}
}
{
System.out.println("构造2");
}
static{
System.out.println("静态2");
}
}
8.8.2 静态变量和实例变量的区别
在语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加。
在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
8.8.3 向上转型和向下转型
在JAVA中,继承是一个重要的特征,通过extends关键字,子类可以复用父类的功能,如果父类不能满足当前子类的需求,则子类可以重写父类中的方法来加以扩展。
在应用中就存在着两种转型方式,分别是:向上转型和向下转型。
比如:父类Parent,子类Child
向上转型:父类的引用指向子类对象Parent p=new Child();
说明:向上转型时,子类对象当成父类对象,只能调用父类的功能,如果子类重写了父类的方法就根据这个引用指向调用子类重写方法。
向下转型(较少):子类的引用的指向子类对象,过程中必须要采取到强制转型。
Parent p = new Child();//向上转型,此时,p是Parent类型
Child c = (Child)p;//此时,把Parent类型的p转成小类型Child
//其实,相当于创建了一个子类对象一样,可以用父类的,也可以用自己的
说明:向下转型时,是为了方便使用子类的特殊方法,也就是说当子类方法做了功能拓展,就可以直接使用子类功能。
8.8.4 设计多态的程序
假设现在有一个汽车类,我们可以根据汽车类创建很多汽车对象。
1、 创建汽车类。提供启动、停止、运行功能
2、 创建子类,继承汽车类。覆盖/重写 启动和停止功能
3、 创建子类对象,进行子类的功能测试
4、 创建多态对象,进行功能测试
8.8.5 了解自定义异常
现有的异常类型,不能表示具体的错误,我们还可以自定义异常。自定义异常,通常用新的异常类型名称来表示业务上具体的错误。
Ø 合适的类名
Ø 选择合适的父类
Ø 添加合适的构造方法
package seday13;
//1,继承父类
//2,参考父类的构造创建子类的构造
//generator constructors from superclass
public class PwdNFException extends Exception{
public PwdNFException() {
super();
}
public PwdNFException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public PwdNFException(String message, Throwable cause) {
super(message, cause);
}
public PwdNFException(String message) {
super(message);
}
public PwdNFException(Throwable cause) {
super(cause);
}
}
9 面向对象4
9.1 抽象类
9.1.1 概念
Java中可以定义没有方法体的方法,该方法由其子类来具体的实现。该没有方法体的方法我们称之为抽象方法,含有抽象方法的类我们称之为抽象类。
抽象类可以理解为是一个只有方法声明没有方法体的特殊类。
举例:水果,东西。。
修饰符 abstract 返回值 方法名(参数列表);
class A{
public void eat(){//声明一样,可以提取
syso("eat...B") }
}
class B{
public void eat(){//声明一样,可以提取
syso("eat。。。A") }
}
abstract class C{
public abstract void eat();
}
9.1.2 特点
1、 通过java关键字abstract实现
2、 可以修饰方法或者类
3、 抽象类中可以没有抽象方法(由子类去实现)
4、 如果类中有抽象方法,那该类必须定义为一个抽象类
5、 子类继承了抽象类以后,要么还是一个抽象类,要么就把所有抽象方法都重写
6、 多用于多态中
7、 抽象类不可以被实例化
9.2 抽象类的用法
9.2.1 构造函数
抽象类也有构造方法,但是不能本身实例化。
那抽象类的构造函数有啥用?一般用于给子类实例化。
package day009;
//抽象类的构造方法
public class Test2_Animal2 {
}
abstract class Animal2{
//抽象类可以有构造方法,但是无法实例化
//用于子类实例化
public Animal2(){
System.out.println("fu..Animal2()");
}
}
class Zi2 extends Animal2{
}
class TestAnimal2{
public static void main(String[] args) {
// Animal2 a2 = new Animal2();//抽象类无法实例化
// Zi2 z=new Zi2();//创建子类实例化对象
Animal2 a2 = new Zi2();//抽象类多用于多态
}
}
9.2.2 抽象类的成员变量
既可以有变量,也可以有常量。
package day009;
//成员变量
public class Test3_Animal3 {
}
abstract class Animal3{
//String name; //1
//String name="大黄"; //2
private String name="大黄"; //3
public final int age=10;
//3.1如果是私有变量想要取值,就提供getXxx()
public String getName(){
return name;
}
}
class Zi3 extends Animal3{
}
class Test3Demo{
public static void main(String[] args) {
Animal3 a = new Zi3();//抽象类多用于多态
//System.out.println(a.name()); //1或者2
System.out.println(a.getName()); //3
System.out.println(a.age);
}
}
9.2.3 抽象类的成员方法
抽象类里,既可以有普通方法,有可以有抽象方法。
package day009;
//成员方法
public class Test4_Animal4{
}
abstract class Animal4{
//抽象类中的普通方法
public void speek(){
System.out.println("fu...speak()");
}
//抽象类里的抽象方法
public abstract void study();
}
class Zi4 extends Animal4{
//重写抽象方法
public void study(){
System.out.println("zi...study()");
}
}
class Zi4Test{
public static void main(String[] args) {
Zi4 z= new Zi4();
z.speek();//fu...speak()
z.study();//zi...study()
}
}
9.3 接口
9.3.1 概念
Java里面由于不允许多重继承,所以如果要实现多个类的功能,则可以通过实现多个接口来实现。
Java接口和Java抽象类代表的就是抽象类型,就是我们需要提出的抽象层的具体表现。OOP面向对象的编程,如果要提高程序的复用率,增加程序的可维护性,可扩展性,就必须是面向接口的编程,面向抽象的编程,正确地使用接口、抽象类这些太有用的抽象类型做为java结构层次上的顶层。
interface 接口名{ 代码… }
9.3.2 特点
1、 接口中都是抽象方法
2、 通过interface关键字创建接口
3、 通过implements让子类来实现
4、 可以理解成,接口是一个特殊的抽象类
5、 接口突破了java的单继承的局限性
6、 接口和类之间可以多实现,接口和接口之间可以多继承
7、 接口是对外暴露的规则,是一套开发规范
8、 接口提高了程序的功能扩展,降低了耦合性
9.3.3 入门案例
package day9999;
public class T {
public static void main(String[] args) {
Zi z = new Zi();
z.study();
z.teach();
}
}
interface Fu{
public abstract void study();
public abstract void teach();
}
//实现+重写
class Zi implements Fu{
public void study(){
System.out.println("Zi..study()");
}
public void teach(){
System.out.println("Zi..teach()");
}
}
9.4 接口的用法
9.4.1 构造方法
接口里是没有构造方法的。
在创建实现类的对象时默认的super(),是调用的默认Object的无参构造。
interface Fu{//定义一个接口
public abstract void show();
//Interfaces cannot have constructors
/*public Fu(){
System.out.println("Fu.Fu()");
}*/
}
9.4.2 成员变量
接口里没有成员变量,都是常量。所以,你定义一个变量没有写修饰符时,默认会加上:
public static final
package day009;
//构造函数,成员方法,成员变量
public interface Test7_Fu {
}
interface Fu2{//定义一个接口
//int num=10;//1,成员变量
//static int num=10;//2,默认就是静态的
//final static int num=10;//3,默认就是final的
public final static int num=10;//4,默认就是public的
class Zi7 implements Fu2{
}
class Test7Demoo{
public static void main(String[] args) {
Zi7 z= new Zi7();
//The final field Fu2.num cannot be assigned
//z.num=30;//默认是final的,不能修改值
System.out.println(z.num);
System.out.println(Fu2.num);
}
}
9.4.3 接口的成员方法
接口里的方法,默认就都是抽象的,如果你不写明是abstract的,那会自动补齐。
例如:abstract void save
9.4.4 总结
1、 类与类的关系:
– 是继承关系,java只支持单继承 class A extends B
2、 类和接口的关系:
– 实现关系 class x implements A
– 类需要把A接口里的所有抽象方法重写
– 多实现 class x implements A,B
– 类需要把A,B接口里的所有抽象方法重写
– 可以在继承的同时多实现,注意要先继承后实现
//A是类,B也是类,C和D是接口
class A extends B implements C,D{
3、 接口和接口的关系:
– 继承关系 interface c extends A
– 多继承 interface c extends A,B
9.5 设计模式
Java中有23 种设计模式,本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。
当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择。
1、 对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易。
2、 但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。
9.5.1 单例设计模式概念
单例模式可以说是大多数开发人员在实际中使用最多的,常见的Spring默认创建的bean就是单例模式的。
单例模式有很多好处,比如可节约系统内存空间,控制资源的使用。
其中单例模式最重要的是确保对象只有一个。
简单来说,保证一个类在内存中的对象就一个。
RunTime就是典型的单例设计,我们通过对RunTime类的分析,一窥究竟。
9.5.2 源码剖析
package java.lang;
public class Runtime {
//1、创建静态的全局唯一的对象
private static Runtime currentRuntime = new Runtime();
//2、私有构造方法,
/** Don't let anyone else instantiate this class */
private Runtime() {}
//3、通过自定义的静态方法获取实例
public static Runtime getRuntime() {
return currentRuntime;
}
}
9.5.3 饿汉式
目的:控制外界创建对象的个数只能创建1个对象
开发步骤:
1、 私有化构造方法
2、 在类的内部创建好对象
3、 对外界提供一个公共的get(),返回一个已经准备好的对象
package cn.tedu.single;
//测试单例设计模式
public class Test8_Single {
public static void main(String[] args) {
Single s = Single.get();
Single s1 = Single.get();
//get()多少次,内存中使用的都是同一个对象
System.out.println(s);//cn.tedu.single.Single@15db9742
System.out.println(s1);//cn.tedu.single.Single@15db9742
}
}
class Single{
// 1、私有化构造方法,不让外界直接new
private Single() {}
// 2、在类的内部,创建好对象
//static :静态只能调用静态
static private Single s = new Single();
// 3、对外界提供一个公共的get(),返回一个已经准备好的对象
//static是为了外界不通过对象访问而是通过类名直接方法
static public Single get(){
//注意:静态只能调用静态
return s;
}
}
9.5.4 懒汉式
class Single{
// 1、私有化构造方法,不让外界直接new
private Single() {}
// 2、在类的内部,创建好对象
//static :静态只能调用静态
static private Single s = null;
// 3、对外界提供一个公共的get(),返回一个已经准备好的对象
//static是为了外界不通过对象访问而是通过类名直接方法
synchronized static public Single get(){
//注意:静态只能调用静态
if(s==null){
s = new Single();//会有安全问题
}
return s;
}
}
9.5.5 测试
改造代码形成模板,减少代码量
abstract class X{
public void method() {
long s = System.currentTimeMillis();
eat();
long t = System.currentTimeMillis() - s;
System.out.println(t);
}
abstract public void eat();
}
class Y extends X{
public void eat() {//模板中的某些细节交给子类自己实现
for (int i = 0; i < 1000000; i++) {
System.out.println();
}
}
}
9.6 拓展
9.6.1 abstract注意事项
抽象方法要求子类继承后必须重写。那么,abstract关键字不可以和哪些关键字一起使用呢?以下关键字,在抽象类中。用是可以用的,只是没有意义了。
1、 private:被私有化后,子类无法重写,与abstract相违背。
2、 static:静态的,优先于对象存在。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
3、 final:被final修饰后,无法重写,与abstract相违背。
9.6.2 接口和抽象类的区别
1、抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现,接口是设计的结果 ,抽象类是重构的结果
7、抽象类里可以没有抽象方法,如果要扩展抽象类的新方法,子类将很容易的就能得到这些新方法。
8、如果一个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可继承接口,并可多继承接口,但类只能单根继承。
10 API1
API:Application Programming Interface应用编程接口,一切可以调用的东西都是API。
java.lang包,这个包会自动导入。
java.lang.Object
java.lang.String
java.lang.StringBuilder/StringBuffer
正则表达式
包装类等
10.1 Object
10.1.1 概念
所有对象的顶级父类
存在于java.lang包中,这个包不需要我们手动导包
10.1.2 常用方法
boolean equals(Object obj) 指示其他某个对象是否与此对象“相等”。
protected void finalize() 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
int hashCode() 返回该对象的哈希码值。
String toString() 返回该对象的字符串表示
10.1.3 toString()
默认返回 类名@地址 的格式,来展示对象的地址值,如:a00000.Student@a0834e7。
如果想看属性值我们可以重写这个方法,重写后返回的就是把属性值拼接成一个字符串。
如:Student [name=苏大强, age=20, id=10001]
package cn.tedu.object;
//测试Object用法
public class Test1_Object {
public static void main(String[] args) {
//创建对象测试
Person p = new Person("熊大",20,10,"森林");
System.out.println(p);//要是不重写toString()就是使用的Object提供的默认方式只能打印对象在内存的地址
System.out.println(p);//由于重写了toString()所以打印的是属性的值
}
}
//定义类,提供属性
class Person{
//构造:source-generate constrctor using fileds
public Person() {}
public Person(String name, int age, double salary, String addr) {
this.name = name;
this.age = age;
this.salary = salary;
this.addr = addr;
}
//属性
private String name;
private int age;
private double salary;
private String addr;
//set() get()
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 double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
//toString():source - generate toString()-ok
@Override//重写的是Object提供的toString()是为了查看属性值
public String toString() {
return "Person [name=" + name + ", age=" + age + ", salary=" + salary + ", addr=" + addr + "]";
}
}
10.1.4 equals(Object obj)
当前对象和参数对象比较大小,默认是比较内存地址,如果要比较对象的属性,可以重写该方法。
package javase.base;
import javase.base.extend.Tiger;
public class TestArea {
public static void main(String[] args) {
//基础类型是按值比较
int i1 = 10;
int i2 = 10;
int i3 = 12;
System.out.println(i1==i2);//true
System.out.println(i1==i3); //false
//对象为引用类型,引用类型按内存地址比较
Tiger t1 = new Tiger();
Tiger t2 = new Tiger();
Tiger t3 = t1;
System.out.println(t1);
System.out.println(t2);
System.out.println(t3);
System.out.println(t1.equals(t2));//false
System.out.println(t1 == t2);//false
System.out.println(t1.equals(t3));//true
System.out.println(t1 == t3);//true
}
}
10.1.5 hashCode()
返回该对象的哈希码值。
package cn.tedu.object;
//测试equals() ==
public class Test2_Object2 {
public static void main(String[] args) {
//1,==比较八大基本类型时:判断的是值
int a = 1;
int b = 1;
System.out.println(a==b);//true
//2,==比较对象时,比较的是对象的地址值
Integer x = 5;
Integer y = 10;
Integer z = x;//把x的地址赋值给了z
System.out.println(x==y);//false
System.out.println(x==z);//true
//3,equals()用来比较对象存着的值
System.out.println(x.equals(z));//true
//4,测试hashCode()哈希值
System.out.println(x.hashCode());//5
System.out.println(y.hashCode());//10
System.out.println(z.hashCode());//5
}
}
10.2 String
字符串对象
10.2.1 特点
是一个封装char[]数组的对象
字符串不可变
10.2.2 创建String对象
1、 如果是第一次使用字符串,java会在字符串常量池创建一个对象。
2、 再次使用相同的内容时,会直接访问常量池中存在的对象。
方式1:new String(char[])
其实字符串底层维护了一个char[]
char[] c = {'a','b','c','d'};
String s = new String(c);//堆中分配新的内存
System.out.println(s);
方式2:直接创建
常量池里直接创建对象(本质还是char[]),再次使用相同内容,会去常量池中找到已经存在的对象,不会新建。
String s2="abcd";//常量池中分配新的内存
System.out.println(s2);
System.out.println(s==s2);//地址不同
System.out.println(s.equals(s2));//内容相同
//如果使用过了就不再创建,引用存在的对象
String s3="abcd";//访问常量池中已经存在的对象
System.out.println(s3==s2);//true
10.2.3 字符串连接效率
利用String类,在做字符串拼接的过程效率极其低下。
String s1="aaa";
String s2="bbb";
String s3="ccc";
String s4=s1+s2+s3;//字符串不可变,每次加会创建新对象,这行代码要产生右侧的5个新对象,慢
package a00000;
public class TT {
public static void main(String[] args) {
String s = "abcdefghijklmnopqrstuvwxyz";
String news="";//字符串是不能被修改的,拼接时,每次会创建对象,
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
news += s;
}
long end = System.currentTimeMillis();
System.out.print(end-start);
}
}
10.2.4 常用方法
length()
charAt()
lastIndexOf()
substring()
equals()
startsWith()
endsWith()
split()
trim() 去除字符串两端的空格
10.2.5 测试
package cn.tedu.String;
import java.util.Arrays;
import java.util.Iterator;
//String的常用方法
public class Test4_String2 {
public static void main(String[] args) {
String s = "abcdefghijk";
System.out.println(s.charAt(3));//d,返回指定索引处的 char 值。
System.out.println(s.endsWith("jk"));//true,是否以指定后缀结束
String s1 = "abc";
System.out.println(s.equals(s1));//false,判断两个字符串的内容是否相同
System.out.println(s.length());//11,字符串的长度
String s2="1,2,3,4,5";
String[] strs = s2.split(",");//根据指定的规则切割字符串
System.out.println(Arrays.toString(strs));//查看数组元素
System.out.println(s2.startsWith("1,"));//true
//截取时含头不含尾[3,7)
System.out.println(s.substring(3,7));//defg
//把指定的参数转换成String类型
//把数字10转成字符串10
System.out.println(String.valueOf(10));
}
}
10.3 StringBuilder/StringBuffer
10.3.1 特点
1、 封装了char[]数组
2、 是可变的字符序列
3、 提供了一组可以对字符内容修改的方法
4、 常用append()来代替字符串做字符串连接
5、 内部字符数组默认初始容量是16:initial capacity of 16 characters
6、 如果大于16会尝试将扩容,新数组大小原来的变成2倍+2,容量如果还不够,直接扩充到需要的容量大小。int newCapacity = value.length * 2 + 2;
7、 StringBuffer 1.0出道线程安全,StringBuilder1.5出道线程不安全
10.3.2 练习:测试字符串连接
package day010;
public class Test5_SB {
public static void main(String[] args) {
String s="abcdefghijklmnopqrstuvwxyz";
StringBuilder sb=new StringBuilder();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.append(s);//在后面空白处,添加
}
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
10.3.3 方法
append()
charAt()
10.4 包装类
10.4.1 与基本类型的对应关系
10.4.2 Number
数字包装类的抽象父类。
子类:
常用的方法:
提供了各种获取值的方式,已经完成了强转。
10.4.3 Integer
创建对象
new Integer(5);新建对象
Integer.valueOf(5);
在Integer类中,包含256个Integer缓存对象,范围是 -128到127。
使用valueOf()时,如果指定范围内的值,访问缓存对象,而不新建;如果指定范围外的值,直接新建对象。
Integer a = new Integer(5);//创建对象
Integer b = Integer.valueOf(5);//读取缓存
Integer c = Integer.valueOf(5);//读取缓存
System.out.println(bc);//true
System.out.println(ab);//false
System.out.println(a.equals(b));//true
方法
parseInt();字符串转换成int
toBinaryString();把整数转换成2进制数据
toOctalString();把整数转换成8进制数据
toHexString();把整数转换成16进制数据
10.5 日期类Date
10.5.1 概述
存在于java.util.Date包。
用来封装一个毫秒值表示一个精确的时间点。
从1970-1-1 0点开始的毫秒值。
10.5.2 创建对象
new Date():封装的是系统当前时间的毫秒值
new Date(900000000000L):封装指定的时间点
10.5.3 常用方法
getTime():取内部毫秒值
setTime():存取内部毫秒值
getMonth():获取当前月份
getHours():获取当前小时
compareTo(Date):当前对象与参数对象比较。当前对象大返回正数,小返回负数,相同0。
10.5.4 练习1:测试日期类的常用方法
package cc;
import java.util.Date;
public class bb {
public static void main(String[] args) {
Date d = new Date();
System.out.println(d);//输出当前时间
System.out.println(d.getTime());//输出1970-1-1至今的毫秒值
System.out.println(d.getMonth());//获取当前月份
System.out.println(d.getHours());//获取当前小时
}
}
10.6 日期工具SimpleDateFormat
10.6.1 概述
日期格式化工具,可以把Date对象格式化成字符串,也可以日期字符串解析成Date对象。
10.6.2 创建对象
new SimpleDateFormat(格式)
格式:yyyy-MM-dd HH:mm:ss
MM/dd/yyyy..
10.6.3 常见方法
format(Date):把Date格式化成字符串
parse(String):把String解析成Date
10.7 BigDecimal/BigInteger
10.7.1 概述
BigDecimal:常用来解决精确的浮点数运算。
BigInteger:常用来解决超大的整数运算。
10.7.2 创建对象
BigDecimal.valueOf(2);
10.7.3 常用方法
add(BigDecimal bd): 做加法运算
substract(BigDecimal bd) : 做减法运算
multiply(BigDecimal bd) : 做乘法运算
divide(BigDecimal bd) : 做除法运算
divide(BigDecimal bd,保留位数,舍入方式):除不尽时使用
setScale(保留位数,舍入方式):同上
pow(int n):求数据的几次幂
10.7.4 练习1:测试常用方法
接收用户输入的两个数字,做运算
package seday11;
import java.math.BigDecimal;
import java.util.Scanner;
public class Test2_BigD {
public static void main(String[] args) {
double a = new Scanner(System.in).nextDouble();
double b = new Scanner(System.in).nextDouble();
System.out.println(a+b);
System.out.println(a-b);
System.out.println(a*b);
System.out.println(a/b);//不精确
System.out.println(“===上面的除法不精确===”);
BigDecimal bd1 = BigDecimal.valueOf(a);
BigDecimal bd2 = BigDecimal.valueOf(b);
BigDecimal bd3;
bd3=bd1.add(bd2);
System.out.println(bd3.doubleValue());
bd3=bd1.subtract(bd2);
System.out.println(bd3.doubleValue());
bd3=bd1.multiply(bd2);
System.out.println(bd3.doubleValue());
// bd3=bd1.divide(bd2);//报错除不尽
//保留位数和舍入方式
bd3=bd1.divide(bd2,5,BigDecimal.ROUND_HALF_UP);
bd3=bd3.setScale(2, BigDecimal.ROUND_HALF_UP);//保留两位
System.out.println(bd3.doubleValue());
}
}
10.8 拓展
10.8.1 了解进制
10.8.2 StringBuilder和StringBuffer的区别
1、 在线程安全上,
–StringBuffer是旧版本就提供的,线程安全的。@since JDK1.0
–StringBuilder是jdk1.5后产生,线程不安全的。@since 1.5
2、 在执行效率上,StringBuilder > StringBuffer > String
3、 源码体现:本质上都是在调用父类抽象类AbstractStringBuilder来干活,只不过Buffer把代码加了同步关键字,使得程序可以保证线程安全问题。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
10.8.3 自动装箱和自动拆箱
自动装箱:把基本类型包装成一包装类的对象
Integer a = 5;//a是引用类型,引用了包装对象的地址。
编译器会完成对象的自动装箱:Integer a = Integer.valueOf(5);
自动拆箱:从包装对象中,自动取出基本类型值
int i = a;//a现在是包装类型,没法给变量赋值,需要把5取出来。
编译器会完成自动拆箱:int i = a.intValue();
11 IO
11.1 IO简介
11.1.1 继承结构
in/out相对于程序而言的输入(读取)和输出(写出)的过程。
在Java中,根据处理的数据单位不同,分为字节流和字符流
java.io包:
File
字节流:针对二进制文件
InputStream
–FileInputStream
–BufferedInputStream
–ObjectInputStream
OutputStream
–FileOutputStream
–BufferedOutputStream
–ObjectOutputStream
字符流:针对文本文件。读写容易发生乱码现象,在读写时最好指定编码集为utf-8
Writer
–BufferedWriter
–OutputStreamWriter
Reader
–BufferedReader
–InputStreamReader
–PrintWriter/PrintStream
11.1.2 流的概念
数据的读写抽象成数据,在管道中流动。
Ø 流只能单方向流动
Ø 输入流用来读取in
Ø 输出流用来写出Out
Ø 数据只能从头到尾顺序的读写一次
11.2 File文件流
11.2.1 概述
封装一个磁盘路径字符串,对这个路径可以执行一次操作。
可以用来封装文件路径、文件夹路径、不存在的路径。
11.2.2 创建对象
File(String pathname) 通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
11.2.3 常用方法
文件、文件夹属性
length():文件的字节量
exists():是否存在,存在返回true
isFile():是否为文件,是文件返回true
isDirectory():是否为文件夹,是文件夹返回true
getName():获取文件/文件夹名
getParent():获取父文件夹的路径
getAbsolutePath():获取文件的完整路径
创建、删除
createNewFile():新建文件,文件夹不存在会异常,文件已经存在返回false
mkdirs():新建多层不存在的文件夹\a\b\c
mkdir():新建单层不存在的文件夹\a
delete():删除文件,删除空文件夹
文件夹列表
ist():返回String[],包含文件名
listFiles():返回File[],包含文件对象
11.2.4 练习2:递归求目录总大小
递归:不断的调用方法本身。
递归:统计文件大小,删除文件
求目录的总大小:
1、把指定目录封装成File对象
2、把文件夹列表列出来
3、判断,如果是文件,直接把f.length()相加
4、判断,如果是文件夹,继续列表,继续判断,如果是文件相加,如果又是文件夹,继续列表,继续判断,如果是文件相加......
5、如果是文件夹,递归调用方法本身的业务逻辑
package cn.tedu.io;
import java.io.File;
import org.junit.Test;
//递归求目录总大小
public class Test2_File2 {
public static void main(String[] args) {
// 1、把指定目录封装成File对象
File file = new File("D:\\teach\\a");
int size =count(file);
System.out.println(size);
}
private static int count(File file) {
// 2、把文件夹列表列出来
File[] files = file.listFiles();
//2.1 遍历数组里的每个资源
int sum = 0;//记录文件的大小
for (int i = 0; i < files.length; i++) {
// 3、判断,如果是文件,直接把f.length()相加
// files[i]表示每次遍历到的资源
if(files[i].isFile()) {
sum += files[i].length();//求文件的和
}else if(files[i].isDirectory()){
// 4、判断,如果是文件夹,继续列表,继续判断,如果是文件相加,如果又是文件夹,继续列表,继续判断,如果是文件相加......
// 5、如果是文件夹,递归调用方法本身的业务逻辑
sum += count(file[i]);//把当前遍历到的文件夹继续循环判断求和
}
}
return sum ;
}
}
11.3 字节流读取
字节流是由字节组成的,字符流是由字符组成的. Java里字符由两个字节组成.字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据。
流式传输主要指将整个音频和视频及三维媒体等多媒体文件经过特定的压缩方式解析成一个个压缩包,由视频服务器向用户计算机顺序或实时传送。在采用流式传输方式的系统中,用户不必像采用下载方式那样等到整个文件全部下载完毕,而是只需经过几秒或几十秒的启动延时即可在用户的计算机上利用解压设备对压缩的A/V、3D等多媒体文件解压后进行播放和观看。此时多媒体文件的剩余部分将在后台的服务器内继续下载。
11.3.1 InputStream抽象类
此抽象类是表示字节输入流的所有类的超类/抽象类。
常用方法:
abstract int read()从输入流中读取数据的下一个字节。
int read(byte[] b) 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。
int read(byte[] b, int off, int len) 将输入流中最多 len 个数据字节读入 byte 数组。
void close() 关闭此输入流并释放与该流关联的所有系统资源。
11.3.2 FileInputStream子类
直接插在文件上,直接读取文件数据。
创建对象
**FileInputStream(File file)**通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
FileInputStream(String pathname) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
11.3.3 BufferedInputStream子类
BufferedInputStream 为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组(默认8M大小)。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。
创建对象
BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。
1.4 练习:文件的读取
读取指定文件
package cn.tedu.hello;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
public class tt {
public static void main(String[] args) throws Exception {
method1();// 字节读取
}
private static void method1() throws Exception {
long s = System.currentTimeMillis();
InputStream in = new FileInputStream("D:\\teach\\1.jpg");
int b = 0;
while ((b = in.read()) != -1) {
// System.out.println(b);
}
s = System.currentTimeMillis() - s;
System.out.println(s + "--");// 7515
long ss = System.currentTimeMillis();
InputStream in2 = new BufferedInputStream(new FileInputStream("D:\\teach\\1.jpg"));
int b2 = 0;
while ((b2 = in2.read()) != -1) {
// System.out.println(b2);
}
ss = System.currentTimeMillis() - ss;
System.out.println(ss + "==");// 32
in.close();
in2.close();
}
}
11.5 字节流写出
11.5.1 OutputStream抽象类
此抽象类是表示输出字节流的所有类的超类。输出流接受输出字节并将这些字节发送到某个接收器。
常用方法:
void close() 关闭此输出流并释放与此流有关的所有系统资源。
void flush() 刷新此输出流并强制写出所有缓冲的输出字节。
void write(byte[] b) 将 b.length 个字节从指定的 byte 数组写入此输出流。
void write(byte[] b, int off, int len)将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
abstract void write(int b) 将指定的字节写入此输出流。
11.5.2 FileOutputStream子类
直接插在文件上,直接写出文件数据
创建对象:
**FileOutputStream(String name)**创建一个向具有指定名称的文件中写入数据的输出文件流。
**FileOutputStream(File file)**创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
FileOutputStream(File file, boolean append) –追加 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
11.5.3 BufferedOutputStream子类
该类实现缓冲的输出流。通过设置这种输出流,应用程序就可以将各个字节写入底层输出流中,而不必针对每次字节写入调用底层系统。
创建对象
**BufferedOutputStream(OutputStream out)**创建一个新的缓冲输出流,以将数据写入指定的底层输出流。
11.6 练习:文件的写出
把数据写出到指定文件中。如果文件不存在会自动创建,文件夹不存在会报错。
package cn.tedu.hello;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
public class rr {
public static void main(String[] args) throws Exception {
// method1();//字节写出
}
private static void method1() throws Exception {
long s = System.currentTimeMillis();
OutputStream out = new FileOutputStream(new File("D:\\teach\\a.txt"));
for(int i = 48 ; i < 1000000; i++) {
out.write(i);
}
s = System.currentTimeMillis() - s;
System.out.println(s + "--");//3484
long ss = System.currentTimeMillis();
OutputStream out2 = new BufferedOutputStream(new FileOutputStream(new File("D:\\teach\\a2.txt")));
for(int i = 48 ; i < 1000000; i++) {
out2.write(i);
}
ss = System.currentTimeMillis() - ss;
System.out.println(ss + "==");//54
out.close();
out2.close();
}
}
11.7 扩展1
11.7.1 了解字符流读写
11.7.2 常见字符编码表
11.7.3 JDK1.7新特性之IO关流
try( ){ }catch(){ }
private static void customBufferStreamCopy(File source, File target) {
InputStream fis = null;
OutputStream fos = null;
try {
fis = new FileInputStream(source);
fos = new FileOutputStream(target);
byte[] buf = new byte[8192];
int i;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
}
catch (Exception e) {
e.printStackTrace();
} finally {
close(fis);
close(fos);
}
}
private static void close(Closeable closable) {
if (closable != null) {
try {
closable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上述代码对于异常处理十分复杂,
对于资源的关闭也很麻烦,那么可以和下面的进行对比:
private static void customBufferStreamCopy(File source, File target) {
try (InputStream fis = new FileInputStream(source);
OutputStream fos = new FileOutputStream(target)){
byte[] buf = new byte[8192];
int i;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
11.8 IO综合练习
11.8.1 练习1:文件复制
from,to。读取from的数据。写出到to文件里
package cn.tedu.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
//文件复制
public class Test4_Copy {
public static void main(String[] args) throws Exception {
// 1,创建读取文件和写出文件
File from = new File("D:\\teach\\a\\1.txt");
File to = new File("D:\\teach\\a\\to.txt");
//调用copy完成文件复制
copy(from, to);
}
//封装了文件复制的工具,将来可以通过类名.直接调用
public static void copy(File from, File to) throws Exception {
// 2,读取from,写出到to
InputStream in = new FileInputStream(from);
OutputStream out = new FileOutputStream(to);
// 3,开始读,读到-1为止
int b = 0;// 记录每次读取到的数据
while ((b = in.read()) != -1) {
out.write(b);// 把读到的内容写出去
}
// 4,关闭资源
in.close();
out.close();
}
}
1.8.2 练习2:批量读写
package cn.tedu.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
//文件复制
public class Test4_Copy {
public static void main(String[] args) throws Exception {
// 1,创建读取文件和写出文件
File from = new File("D:\\teach\\a\\1.txt");
File to = new File("D:\\teach\\a\\to.txt");
copyByte(from, to);// 一个字节一个自己的复制
copyArray(from, to);// 一个数组一个数组的复制
}
// 一个数组一个数组的复制
private static void copyArray(File from, File to) throws Exception {
// 2,读取from,写出到to
InputStream in = new FileInputStream(from);
OutputStream out = new FileOutputStream(to);
// 3,批量的读和写
int b = 0;// 记录每次读取到的数据
//源码:数组默认的长度一般是8M数组的长度就是8*1024
byte[] bs = new byte[8*1024];//用来缓存数据
while ((b = in.read(bs)) != -1) {//读取数组中的内容
out.write(bs);// 把读到的数组里的内容写出去
}
// 4,关闭资源
in.close();
out.close();
}
// 封装了文件复制的工具,将来可以通过类名.直接调用
public static void copyByte(File from, File to) throws Exception {
// 2,读取from,写出到to
InputStream in = new FileInputStream(from);
OutputStream out = new FileOutputStream(to);
// 3,开始读,读到-1为止
int b = 0;// 记录每次读取到的数据
while ((b = in.read()) != -1) {
out.write(b);// 把读到的内容写出去
}
// 4,关闭资源
in.close();
out.close();
}
}
11.9 泛型
11.9.1 概念
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}
public interface Deque<E> extends Queue<E> {}
public interface Queue<E> extends Collection<E> {}
public interface Collection<E> extends Iterable<E> {}
们上面的代码中出现的<?>是什么东西呢 它叫泛型,常用来和集合对象一同使用,所以我们在开始学习集合之前,必须先了解下什么是泛型。而且泛型概念非常重要,它是程序的增强器,它是目前主流的开发方式。
泛型是(Generics)是JDK1.5 的一个新特性,其实就是一个『语法糖』,本质上就是编译器为了提供更好的可读性而提供的一种小手段,小技巧,虚拟机层面是不存在所谓『泛型』的概念的。
11.9.2 作用
l 通过泛型的语法定义,约束集合元素的类型,进行安全检查,把错误显示在编译期
l 代码通用性更强,后面有案例
l 泛型可以提升程序代码的可读性,但它只是一个语法糖(编译后这样的东西就被删除,不出现在最终的源代码中),对于JVM运行时的性能是没有任何影响的。
11.9.3 泛型示例
我们创建一个ArrayList,上面看到eclipse提示有个黄线,什么意思呢?
ArrayList is a raw type. References to generic type ArrayList should be parameterized.
ArrayList使用了泛型,在声明时需指定具体的类型。
那我们把这个<>里的方式就称为泛型。上面的泛型有什么作用呢?就是在编译阶段就检查我们传入的参数类型是否正确。
有了泛型,我们可以看到人家要求存放String,而我故意存放的整数100,所以eclipse提示我们错误:
The method add(int, String) in the type List is not applicable for the arguments (int)。
类型List的add方法要求增加的类型为String类型,不正确不能存入。
11.9.4 泛型声明
泛型可以在接口、方法、返回值上使用:
java.util.List泛型接口/类:
public interface Collection {}
泛型方法的声明:
public void print(E e) {}
在方法返回值前声明了一个表示后面出现的E是泛型,而不是普通的java变量。
11.9.5 常用名称
l E - Element (在集合中使用,因为集合中存放的是元素)
l T - Type(Java 类)
l K - Key(键)
l V - Value(值)
l N - Number(数值类型)
l ? - 表示不确定的java类型
11.9.6 用途:编译时类型检查
package seday12new;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test1 {
public static void main(String[] args) {
int[] a = new int[3];
a[0]=1;
a[1]=2;
//int类型的数组,规定了数组里的数据类型,类型不对就报错。
// a[2]="hello";
//1,泛型的标志<>
//2,泛型的好处:规定了数据的类型,不能想放什么数据就放什么类型,要遵守泛型规定的类型
//3,泛型的数据类型只能是引用类型,不能是基本类型
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
//4,如果类型不对,把运行时期才会 报的错ClassCastException直接在编译时期就报出来
// list.add("a");
// list.add('b');
Iterator it = list.iterator();
while(it.hasNext()) {
Integer s = (Integer) it.next();
System.out.println(s);
}
}
}
11.9.7 用途:代码通用性更强
传统方式通过重载多态实现,方法同名,参数类型不同。
package javase.base.gennarics;
public class TestOldStyle {
public static void print(Integer[] dArray) {
for( Integer d : dArray) {
System.out.println(d);
}
}
public static void print( String[] sArray) {
for( String s : sArray) {
System.out.println(s);
}
}
public static void main(String[] args) {
Integer[] scores = new Integer[]{100,98,80};
String[] names = new String[]{"语文","数学","英语"};
TestOldStyle.print(scores);
TestOldStyle.print(names);
}
泛型方式
package javase.base.gennarics;
public class TestGenarics {
public static <E> void print(E[] arr) {
for(E e : arr) {
System.out.println(e);
}
}
public static void main(String[] args) {
Integer[] scores = new Integer[]{ 100,98,80 };
String[] names = new String[]{ "语文","数学","英语" };
Double[] moneys = new Double[] { 10.1,20.2,30.3 };
TestGenarics.print(scores);
TestGenarics.print(names);
TestGenarics.print(moneys);
}
}
11.9.8 类型擦除
泛型只是在编译期间生存,编译后就被干掉了,真正运行时,大多情况下取而代之的是Object。
下面的代码利用了jdk提供的强大的反射功能,后续会专门详细讲解,今天先初体验下其强大的功能。
package javase.generics;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
//泛型类型擦除
public class TestGenerics {
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<Integer>();
//1. 编译器按泛型检查,类型报错。这是在编译阶段
//list.add("chenzs");
//2. 但在实际运行时,泛型的地方就被替代为通用类型Object
Class<?> clazz = list.getClass();
Method m = clazz.getDeclaredMethod("add", Object.class);
//3. 利用发射得到的对象是运行时对象,其就可以设置非整形的数据
m.invoke(list, "chenzs");
System.out.println(list.get(0));
}
}
11.10 扩展
11.10.1 IO中flush()和close()的区别
close():
所有的流都实现了:
java.io.Closeable接口,都是可关闭的,都有close()方法。
流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,
不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。
close():释放和流有关的资源文件,在关闭该流之前,必须刷新它,而且关闭流之后不能在进行数据的操作,否则:IOException.
flush():
所有的输出流都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法。
养成一个好习惯,输出流在最终输出之后,一定要记得flush()
刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据
强行输出完(清空管道!)刷新的作用就是清空管道。
注意:如果没有flush()可能会导致丢失数据。
flush()方法
-----刷新该流的缓冲区(一般写字符时要用,因为字符是先进入的缓冲区),但并没有关闭该流,刷新之后
流对象还可以继续使用
一般情况下可以直接使用close()方法直接关闭该流,但是当数据量比较大的时候,可以使用flush()方法
对于字符流 一般写入的时候想要马上看到一般需要flush(),
11.10.2 封装释放资源的close()
public static void close(Closeable io) {
if (io != null) {
try {
io.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
11.10.3 BIO、NIO、AIO的区别
阻塞IO,BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
非阻塞IO,NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
异步IO,AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。但目前还不够成熟,应用不多。
11.10.4 数组和链表区别
List是一个接口,它有两个常用的子类,ArrayList和LinkedList,看名字就可以看得出一种是基于数组实现另一个是基于链表实现的。
数组ArrayList遍历快,因为存储空间连续;链表LinkedList遍历慢,因为存储空间不连续,要去通过指针定位下一个元素,所以链表遍历慢。
数组插入元素和删除元素需要重新申请内存,然后将拼接结果保存进去,成本很高。例如有100个值,中间插入一个元素,需要数组重新拷贝。而这个动作对链表来说,太轻松了,改变一下相邻两个元素的指针即可。所以链表的插入和修改元素时性能非常高。
实际开发就根据它们各自不同的特点来匹配对应业务的特点。业务一次赋值,不会改变,顺序遍历,就采用数组;业务频繁变化,有新增,有删除,则链表更加适合
11.10.5 读一行写一行
package game;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintWriter;
//测试
public class aa {
public static void main(String[] args) {
try {
BufferedReader in = new BufferedReader(new FileReader("1.txt"));
String line = in.readLine();
System.out.println(line);//没数据就会读到null
BufferedReader in = new BufferedReader(new FileReader("1.txt"));
PrintWriter out = new PrintWriter("2.txt");
String line;
while ( ( line=in.readLine() ) != null) {
out.println(line);
}
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
12 集合
12.1 Collection接口
12.1.1 概述
英文名称Collection,是用来存放对象的数据结构。其中长度可变,而且集合中可以存放不同类型的对象。并提供了一组操作成批对象的方法。
数组的缺点:长度是固定不可变的,访问方式单一,插入、删除等操作繁琐。
12.1.2 集合的继承结构
Collection接口
– List接口 : 数据有序,可以重复。
– ArrayList子类
– LinkedList子类
– Set接口 : 数据无序,不可以存重复值
– HashSet子类
– Map接口 : 键值对存数据
– HashMap
Collections工具类
2.1.3 常用方法
boolean add(E e):添加元素。
boolean addAll(Collection c):把小集合添加到大集合中 。
boolean contains(Object o) : 如果此 collection 包含指定的元素,则返回 true。
boolean isEmpty() :如果此 collection 没有元素,则返回 true。
Iterator iterator():返回在此 collection 的元素上进行迭代的迭代器。
boolean remove(Object o) :从此 collection 中移除指定元素的单个实例。
int size() :返回此 collection 中的元素数。
Objec[] toArray():返回对象数组
12.1.4 练习1:测试常用方法
package seday11;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Test_301_Collection {
public static void main(String[] args) {
Collection c = new ArrayList();//接口无法直接创建对象
c.add("hello");//添加元素
c.add("java");//添加元素
c.add("~");//添加元素
c.add(10);//jdk5后有自动装箱功能,相当于把10包装Integer.valueOf(10)
System.out.println(c.remove("~"));//移除元素
System.out.println(c.contains("a"));//判断包含关系
System.out.println(c.size());//集合的长度
System.out.println(c);
//for遍历集合
for(int i =0 ;i<c.size();i++) {
System.out.println(c.toArray()[i]);
}
//iterator迭代器遍历
Iterator it = c.iterator();//对 collection 进行迭代的迭代器
while (it.hasNext()) {//如果仍有元素,则返回 true
System.out.println(it.next());//返回迭代获取到的下一个元素
} } }
12.2 List接口
12.2.1 概述
有序的 collection(也称为序列)。此接口的用户可以对列表中每个元素的插入位置进行精确地控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
12.2.2 特点
1、 数据有序
2、 允许存放重复元素
3、 元素都有索引
12.2.3 常用方法
ListIterator listIterator() 返回此列表元素的列表迭代器(按适当顺序)。
ListIterator listIterator(int index) 返回列表中元素的列表迭代器(按适当顺序),从列表的指定位置开始。
**void add(int index, E element)**在列表的指定位置插入指定元素(可选操作)。
boolean addAll(int index, Collection<? extends E> c) 将指定 collection 中的所有元素都插入到列表中的指定位置(可选操作)。
List subList(int fromIndex, int toIndex)返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。
E get(int index) 返回列表中指定位置的元素。
**int indexOf(Object o)**返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。
12.2.4 练习1:测试常用方法
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
//这类用来测试List接口的常用方法
public class Test1_List {
public static void main(String[] args) {
//1、创建List对象
//特点1:List集合元素都有索引,可以根据索引直接定位元素
List list = new ArrayList();
//2、常用方法
list.add(111);
list.add(222);
list.add(333);
list.add(444);
list.add('a');
list.add("abc");
list.add(3,666);//在3的索引处添加指定元素
//特点2:元素有序, 怎么存就怎么放
System.out.println(list);//[111, 222, 333, 666, 444, a, abc]
Object obj = list.get(4);//get(m)-m是索引值,获取指定索引位置的元素
System.out.println(obj);
//3、迭代/遍历集合中的元素
//使用Collection接口提供的iterator()
Iterator it = list.iterator();
while(it.hasNext()) {//判断集合里有没有下个元素
Object o = it.next();
// System.out.println(o);
}
//使用List接口提供的listIterator()
//interfaceListIterator extends Iterator
//区别:可以使用父接口的功能,同时拥有自己的特有功能,不仅顺序向后迭代还可以逆向迭代
ListIterator it2 = list.listIterator();
while(it2.hasNext()) {
Object o = it2.next();
// System.out.println(o);
}
System.out.println();
List list2 = list.subList(1, 3);//subList(m,n)-m是开始索引,n是结束索引,其中含头不含尾类似于String.subString(m,n)
System.out.println(list2);
}
}
12.3 ArrayList
12.3.1 概述
1 存在于java.util包中。
2) 内部用数组存放数据,封装了数组的操作,每个对象都有下标。
3) 内部数组默认初始容量是10。如果不够会以1.5倍容量增长。
4) 查询快,增删数据效率会降低。
12.3.2 创建对象
new ArrayList():初始容量是10
12.3.3 练习1:测试常用方法
常用API,包括下标遍历,迭代器遍历
import java.util.ArrayList;
public class Test3_AL {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("aaa");//存入数据
list.add("123");
list.add("ccc");
list.add("ddd");
System.out.println(list);//list中内容
System.out.println(list.size());//集合长度
System.out.println(list.get(1));//根据下标获取元素
System.out.println();
System.out.println(list.remove(2));//移除下标对应的元素
System.out.println(list);
//下标遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i));
}
//Iterator迭代遍历,封装了下标
Iterator<String> it = list.iterator();
while (it.hasNext()) {//如果有数据
String s = it.next();//一个一个向后遍历
System.out.println(s);
}
}
}
12.4 LinkedList
12.4.1 概述
双向链表,两端效率高。底层就是数组和链表实现的。
12.4.2 常用方法
add()
get()
size()
remove(i)
remove(数据)
iterator()
addFirst() addLast()
getFirst() getLast()
removeFirst() removeLast()
12.4.3 练习1:测试迭代器遍历
双向链表:下标遍历效率低,迭代器遍历效率高
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
public class tt {
public static void main(String[] args) throws Exception {
LinkedList ll = new LinkedList ();
for (int i = 0; i < 100000; i++) {
ll.add(100);
}
f1(ll);
f2(ll);
}
private static void f2(LinkedList<Integer> ll) {
long t = System.currentTimeMillis();
Iterator it = ll.iterator();
while(it.hasNext()) {
it.next();
}
t = System.currentTimeMillis()-t;
System.out.println("=====iterator========="+t);//16
}
private static void f1(LinkedList<Integer> ll) {
long t = System.currentTimeMillis();
for (int i = 0; i < ll.size(); i++) {
ll.get(i);
}
long t1 = System.currentTimeMillis();
System.out.println("~~~~for~~~~~~~"+(t1-t));//9078
}
}
12.5 Set接口
12.5.1 概述
一个不包含重复元素的 collection。
数据无序(因为set集合没有下标)。
由于集合中的元素不可以重复。常用于给数据去重。
12.5.2 特点
Ø HashSet:底层是哈希表,包装了HashMap,相当于向HashSet中存入数据时,会把数据作为K,存入内部的HashMap中。当然K仍然不许重复。
Ø TreeSet:底层就是TreeMap,也是红黑树的形式,便于查找数据。
Ø HashMap实现中,当哈希值相同的对象,会在同一个hash值的位置存储不同属性的数据。
12.5.3 常用方法
boolean add(E e):添加元素。
boolean addAll(Collection c):把小集合添加到大集合中 。
boolean contains(Object o) : 如果此 collection 包含指定的元素,则返回 true。
boolean isEmpty() :如果此 collection 没有元素,则返回 true。
Iterator iterator():返回在此 collection 的元素上进行迭代的迭代器。
boolean remove(Object o) :从此 collection 中移除指定元素的单个实例。
int size() :返回此 collection 中的元素数。
Objec[] toArray():返回对象数组
12.5.4 练习1:测试常用方法
package seday12;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Test0_Map {
public static void main(String[] args) {
Set set = new HashSet ();
set.add("hello");
set.add("b");
set.add("a");
set.add("world");
set.add("b");
//不存重复元素,元素无序
System.out.println(set);
//迭代器
Iterator it = set.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}
12.6 HashSet
12.6.1 概述
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
12.6.2 练习1:获取HashSet里的元素
package seday12;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Test0_Map {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("a");
set.add("e");
set.add("b");
set.add("a");
set.add("b");
System.out.println(set);//无序,不重复
Iterator it = set.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}
12.6.3 练习2:Set存储属性值相同的对象
需求:我们仍然假设相同属性的两个人是同一个人
1、按照以前的经验,这种需求只需要重写equals()方法就可以实现。
2、但是我们提供以后,equals()根本就没有执行。问题出现在新增功能。
3、查找新增的源码发现,其实在添加时只是计算对象的hash值。
4、由于每次创建对象时hash值都不一样,所以每次都会当做新对象存起来。
5、所以,现在我们必须保证两个对象的hash值相同,重写hashCode()。
package seday12;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Test0_Map {
public static void main(String[] args) {
HashSet set = new HashSet();
//创建元素
Student s1 = new Student("西门庆",20);
Student s2 = new Student("武大郎",19);
Student s3 = new Student("潘金莲",21);
Student s4 = new Student("小龙女",23);
Student s5 = new Student("武大郎",19);
Student s6 = new Student("潘金莲",21);
//添加时,新元素会和老元素比
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
//默认:添加时查找对象的hash值,没有查到就存起来
//所以必须让hash值一致才可以
set.add(s5);
set.add(s6);
//问题1:属性相同时还是认为是两个对象...
System.out.println(set);
// 遍历
Iterator it = set.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}
创建Student类
package seday12;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student() {
}
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;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
//需求:属性值都一样就看做是同一个对象
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
//HashSet默认:添加时查找对象的hash值,没有查到就存起来
//所以必须让hash值一致才可以
//必须用算法,不然的话,hash值相同时会挂一串
public int hashCode() {
//return 0;//效率低
//让基本类型*31,引用类型就用自己的hash值
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
}
12.7 扩展
12.7.1 ArrayList扩容
ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10 (如下图一);之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15(如下图二);当添加第16个数据时,继续扩容变为15 * 1.5 =22个
ArrayList没有对外暴露其容量个数,查看源码我们可以知道,实际其值存放在elementData对象数组中,那我们只需拿到这个数组的长度,观察其值变化了几次就知道其扩容了多少次。怎么获取呢?只能用反射技术了。
12.7.2 HashMap扩容
成长因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
前面的讲述已经发现,当你空间只有仅仅为10的时候是很容易造成2个对象的hashcode 所对应的地址是一个位置的情况。这样就造成 2个 对象会形成散列桶(链表)。这时就有一个加载因子的参数,值默认为0.75 ,如果你hashmap的 空间有 100那么当你插入了75个元素的时候 hashmap就需要扩容了,不然的话会形成很长的散列桶结构,对于查询和插入都会增加时间,因为它要一个一个的equals比较。但又不能让加载因子很小,如0.01,这样显然是不合适的,频繁扩容会大大消耗你的内存。这时就存在着一个平衡,jdk中默认是0.75,当然负载因子可以根据自己的实际情况进行调整。
12.7.3 Collections工具类
Collections.sort(List<> list):根据元素的自然顺序 对指定列表按升序进行排序。
Collections.max():根据元素的自然顺序,返回给定 collection 的最大元素。
Collections.min():根据元素的自然顺序 返回给定 collection 的最小元素。
Collections.swap(List,i,j):在指定列表的指定位置处交换元素。
Collections.addAll():
测试
package seday12;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Test4_Collections {
public static void main(String[] args) {
List<String> list = new ArrayList();
//添加多个元素
Collections.addAll(list,
"3","30","23","15","29","12","26");
//元素排序
Collections.sort(list);
//默认是字符顺序:[12,15,23,26,29, 3, 30]
System.out.println(list);
//想自己定义比较方式
Collections.sort(list, new Comparator<String>() {
//自定义比较器,sort()自动调用
@Override
public int compare(String o1, String o2) {
//把字符串转成int比大小
int a = Integer.parseInt(o1);
int b = Integer.parseInt(o2);
//o1大是正数,o1小是负数,相等是0
return a-b;
}
});
System.out.println(list);
}
}
12.8 Map接口
12.8.1 概述
java.util接口 Map<K,V>
类型参数: K - 此映射所维护的键的类型V - 映射值的类型。
也叫哈希表、散列表。常用于存 键值对 结构的数据。其中的键不能重复,值可以重复.
12.8.2 特点
Ø 可以根据键 提取对应的值
Ø 键不允许重复,如果重复值会被覆盖
Ø 存放的都是无序数据
Ø 初始容量是16,默认的加载因子是0.75
12.8.3 继承结构
12.8.4 常用方法
void clear()
从此映射中移除所有映射关系(可选操作)。
boolean containsKey(Object key)
如果此映射包含指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
V get(Object key)
返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
boolean isEmpty()
如果此映射未包含键-值映射关系,则返回 true。
V put(K key, V value)
将指定的值与此映射中的指定键关联(可选操作)。
void putAll(Map<? extends K,? extends V> m)
从指定映射中将所有映射关系复制到此映射中(可选操作)。
V remove(Object key)
如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。
int size()
返回此映射中的键-值映射关系数。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射关系的 Set 视图。
12.8.5 练习1:测试常用方法
package seday12;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Test0_Map {
public static void main(String[] args) {
Map map = new HashMap ();
map.put("001", "钢铁侠");
map.put("002", "蜘蛛侠");
map.put("003", "绿巨人");
map.put("004", "灭霸");
map.put("005", "美国队长");
map.put("005", "凤姐");
System.out.println(map.containsKey("001"));
System.out.println(map.containsValue("美国队长"));
System.out.println(map.isEmpty());
System.out.println(map.get("003"));
System.out.println(map.remove("001"));
System.out.println(map.size());
Map map2 = new HashMap ();
map2.put("999", "刘德华");
map.put(null,null);//可以存入键为null,值也null的数据
map.putAll(map2);
System.out.println(map);
//keySet()返回键的set集合,把map的key形成set集合
Set set = map.keySet();
System.out.println(set);
//map集合的遍历,
//方式1:keySet():把map中的可以放入set集合
//遍历方式1:keySet ()
Set set = m.keySet();
Iterator it = set.iterator();
while(it.hasNext()) {
String key = (String) it.next();
String val = (String) m.get(key);
System.out.println(key+"="+val);
}
//遍历方式2:entrySet ()
Set set2 = m.entrySet();
Iterator it2 = set2.iterator();
while(it2.hasNext()) {
Entry en = (Entry) it2.next();
String key = (String) en.getKey();
String value = (String) en.getValue();
System.out.println(key+"=="+value);
}
}
}
12.9 HashMap
Ø HashMap的键要同时重写hashCode()和equals()
hashCode()用来判断确定hash值是否相同
equals()用来判断属性的值是否相同
– equals()判断数据如果相等,hashCode()必须相同
– equals()判断数据如果不等,hashCode()尽量不同
12.9.1 概述
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。
HashMap底层是一个Entry数组,当存放数据时会根据hash算法计算数据的存放位置。算法:hash(key)%n,n就是数组的长度。
当计算的位置没有数据时,就直接存放,当计算的位置有数据时也就是发生hash冲突的时候/hash碰撞时,采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
12.9.2 练习1:读取HashMap的数据
package seday12;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Test0_Map {
public static void main(String[] args) {
HashMap map = new HashMap ();
map.put(100, "刘德华");
map.put(101, "梁朝伟");
map.put(102, "古天乐");
map.put(103, "周润发");
//遍历方式1:keySet ()
Set set = m.keySet();
Iterator it = set.iterator();
while(it.hasNext()) {
String key = (String) it.next();
String val = (String) m.get(key);
System.out.println(key+"="+val);
}
//遍历方式2:entrySet ()
Set set2 = m.entrySet();
Iterator it2 = set2.iterator();
while(it2.hasNext()) {
Entry en = (Entry) it2.next();
String key = (String) en.getKey();
String value = (String) en.getValue();
System.out.println(key+"=="+value);
}
}
}
12.9.3 练习2:字符串中的字符统计
接收用户输入的一串字符串,统计出现的每个字符的个数
package seday12;
import java.util.HashMap;
import java.util.Scanner;
public class Test2_Count {
public static void main(String[] args) {
//abacbcda
String s = new Scanner(System.in).nextLine();
//a 1 b 2 c 1
HashMap<Character,Integer> map = new HashMap<>();
//遍历字符串获取每个字符
for(int i = 0;i<s.length();i++) {
//1,取出字符串中的每个字符
char c = s.charAt(i);
//拿着字符查个数
Integer count = map.get(c);
//如果取出来是null,就存1,
if(count==null) {
map.put(c, 1);
}else {
//如果取出来有值,计数加1
map.put(c,count+1);
}
}
System.out.println(map);
}
}
13 多线程
13.1 进程
13.1.1 概念
就是正在运行的程序。也就是代表了程序锁占用的内存区域。
13.1.2 特点
l 独立性:进程是系统中独立存在的实体,它可以拥有自己的独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
l 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
l 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
13.2 线程
13.2.1 概念
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程可以开启多个线程。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。
简而言之,一个程序运行后至少一个进程,一个进程里包含多个线程。
如果一个进程只有一个线程,这种程序被称为单线程。
如果一个进程中有多条执行路径被称为多线程程序。
13.2.2 进程和线程的关系
从上图中可以看出一个操作系统中可以有多个进程,一个进程中可以有多个线程,每个进程有自己独立的内存,每个线程共享一个进程中的内存,每个线程又有自己独立的内存。(记清这个关系,非常重要!)
所以想使用线程技术,得先有进程,进程的创建是OS创建的,你能实现吗?不能,一般都是c或者c++语言完成的。
13.3 多线程的特性
13.3.1 随机性
13.3.2 线程状态
线程生命周期,总共有五种状态:
- 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
- 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态;
- 根据阻塞产生的原因不同,阻塞状态又可以分为三种:
a) 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
b) 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
c) 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
13.4 多线程创建1:继承Thread
13.4.1 概述
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。Start()方法是一个native方法,它将通知底层操作系统,最终由操作系统启动一个新线程,操作系统将执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
模拟开启多个线程,每个线程调用run()方法
13.4.2 常用方法
String getName() 返回该线程的名称。
static Thread currentThread() 返回对当前正在执行的线程对象的引用。
void setName(String name) 改变线程名称,使之与参数 name 相同。
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
Thread(String name) 分配新的 Thread 对象。
13.4.3 测试
package seday13new;
public class Test1 {
public static void main(String[] args) {
//3、创建线程对象
ThreadDemo t1 = new ThreadDemo("钢铁侠");
ThreadDemo t2 = new ThreadDemo("美队");
//4、开启线程:谁抢到资源谁就先执行
t1.start();
t2.start();
//t1.run();//当做常规方法调用,且 不会发生多线程现象
}
}
//1、作为Thread的子类,并重写run方法。把多线程的业务写在run方法中
class ThreadDemo extends Thread{
public ThreadDemo() {}
public ThreadDemo(String name) {
super(name);
}
@Override
public void run() {
//2、默认实现是super.run();
for (int i = 0; i < 10; i++) {
System.out.println(getName()+i);
}
}
}
执行结果:
hello0
hello1
hello2
hello3
hello4
hello5
hello6
hello7
hello8
hello1
hello2
hello3
hello9
hello4
hello5
hello6
hello7
hello8
hello9
注意:从上面结果可以确认,start()方法只是通知操作系统线程就绪,具体什么时间执行,操作系统来决定,我们JVM已经控制不了了。
13.5 多线程创建2:实现Runnable接口
13.5.1 概述
如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口。
13.5.2 常用方法
void run() 使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。
13.5.3 测试
package seday13new;
public class Test2 {
public static void main(String[] args) {
MyThread t = new MyThread ();
//2,构造创建对象,传入Runnable子类
Thread target = new Thread(t);
Thread target2 = new Thread(t);
//开启线程
target.start();
target2.start();
}
}
//1,实现Runnable接口,重写run()
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
执行结果:
Thread-0
Thread-2
Thread-1
Thread-5
Thread-8
Thread-6
Thread-7
Thread-4
Thread-3
Thread-9
注意:可以看到执行顺序是乱的,我们已经知道start()方法只是通知操作系统线程就绪,具体什么时间执行,操作系统来决定,我们JVM已经控制不了了。这就是乱序的原因,也是正常的。
13.5.4 比较
13.6 扩展
13.6.1 JVM启动是单线程还是多线程?
多线程,最少要启动main线程和GC线程。
13.6.2 守护线程
l setDaemon(true)
后台线程、守护线程
JVM虚拟机退出条件,是所有前台线程结束,当所有前台线程结束,虚拟机会自动退出
不会等待后台线程结束
例如:垃圾回收器是一个后台线程
package day13;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class Test3_v3 {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println("按回车捅醒 t1");
new Scanner(System.in).nextLine();
t1.interrupt();
}
};
//虚拟机不会等待后代线程结束
//所有前台线程结束时,虚拟机会自动退出
t2.setDaemon(true);
t2.start();
}
static class T1 extends Thread {
@Override
public void run() {
SimpleDateFormat f =
new SimpleDateFormat("HH:mm:ss");
for(int i=0; i<10; i++) {
String s = f.format(new Date());
System.out.println(s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断");
break;
}
}
}
}
}
13.6.3 线程共享访问数据冲突
import java.util.Arrays;
public class Test6 {
static char[] a = {'-','-','-','-','-'};
static char c = '*';
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
while(true) {
for (int i = 0; i < a.length; i++) {
a[i] = c;
}
c = (c=='*'?'-':'*');
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
while(true) {
System.out.println(Arrays.toString(a));
}
}
};
t1.start();
t2.start();
}
}
13.7 售票案例
设计4个售票窗口,总计售票100张。
用多线程的程序设计并写出代码。
13.7.1 方案1:继承Thread
package seday13new;
public class Test3 {
public static void main(String[] args) {
Ticket t = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
//问题:票好像卖重复了,同一张票卖了好多次...
t.start();
t2.start();
t3.start();
t4.start();
}
}
class Ticket extends Thread {
//卖了200张票,变成static的
static private int tic = 100;
@Override
public void run() {
while (true) {
//tic=1时,谁都可以进来,t t2 t3 t4
if (tic > 0) {
try {
//t t2 t3 t4都睡了
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t醒了,tic--=1,tic=0;
//t2醒了,tic--=0,tic=-1;
//t3醒了,tic--=-1,tic=-2;
//t4醒了,tic--=-2,tic=-3;
System.out.println(tic--);
}
}
}
}
13.7.2 方案2:实现Runnable
package seday13new;
public class Test4 {
public static void main(String[] args) {
//只创建一次,就100张票
Ticket2 t = new Ticket2();
Thread target = new Thread(t,"窗口1");
Thread target2 = new Thread(t,"窗口2");
Thread target3 = new Thread(t,"窗口3");
Thread target4 = new Thread(t,"窗口4");
target.start();
target2.start();
target3.start();
target4.start();
}
}
class Ticket2 implements Runnable{
private int tickets=100;
@Override
public void run() {
while(true) {
if(tickets >0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+tic--);
}
}
}
}
13.7.3 问题
1、 每次创建线程对象,都会生成一个tickets变量值是100,创建4次对象就生成了400张票了。不符合需求,怎么解决呢?能不能把tickets变量在每个对象间共享,就保证多少个对象都是卖这100张票。-- 用静态修饰
2、 产生超卖,-1张、-2张。
3、 产生重卖,同一张票卖给多人。
4、 多线程安全问题是如何出现的?常见情况是由于线程的随机性+访问延迟。
5、 以后如何判断程序有没有线程安全问题?在多线程程序中+有共享数据+多条语句操作共享数据。
13.8 同步锁
把有可能出现问题的代码包起来,一次只让一个线程执行。通过sychronized关键字实现同步。
当多个对象操作共享数据时,可以使用同步锁解决线程安全问题。
13.8.1 synchronized
synchronized(对象){
需要同步的代码;
}
13.8.1 特点
1、 前提1,同步需要两个或者两个以上的线程。
2、 前提2,多个线程间必须使用同一个锁。
3、 同步的缺点是会降低程序的执行效率, 为了保证线程安全,必须牺牲性能。
4、 可以修饰方法称为同步方法,使用的锁对象是this。
5、 可以修饰代码块称为同步代码块,锁对象可以任意。
13.8.2 改造
package seday13new;
public class Test4 {
public static void main(String[] args) {
Ticket2 t = new Ticket2();
Thread target = new Thread(t, "窗口1");
Thread target2 = new Thread(t, "窗口2");
Thread target3 = new Thread(t, "窗口3");
Thread target4 = new Thread(t, "窗口4");
target.start();
target2.start();
target3.start();
target4.start();
}
}
class Ticket2 implements Runnable {
private int tic = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
// 把有线程安全问题的代码,用同步关键字包起来
// 原理:用一个对象作为一把锁,给代码上锁,一个线程访问锁代码时,其他线程只能等待锁释放才能进来。
// 多线程间要使用同一把锁才可以真的把代码锁住实现线程安全。
// synchronized (new Object()) {//锁了不同对象
// synchronized (obj) {//锁了同一个对象
//synchronized (Ticket2.class) {//锁了本类,针对于静态
synchronized (this) {
if (tic > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(tic--);
}
}
}
}
}
13.9 线程锁
13.9.1 悲观锁和乐观锁
l 悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
l 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
13.9.2 两种常见的锁
l Synchronized 互斥锁(悲观锁,有罪假设)
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
l ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)
ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。
读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。
13.9.3 Synchronized
使用同步锁实现Synchronized
package javapro.thread;
public class TicketThread extends Thread{
//总票数,多个线程共享这个变量,能修改 ticket–
private int ticket = 10;
//执行业务,重写父类run方法
@Override
public void run() {
//业务处理,卖票:票–
while(true) { //线程非常多,我想尽量给我资源
synchronized (this) { //对象锁
//判断一个条件,出去条件
if(ticket<=0) { //多线程可能ticket=-1
break; //退出死循环
}
//不出现,线程run方法执行太快,不会发生线程冲突
try { //不能抛出异常,抛出就不是重写run方法
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“窗口:” + Thread.currentThread().getName()
+”, 剩余票数:” + ticket-- );
}
}
}
//3个窗口都买这一个票
public static void main(String[] args) {
//目标
Thread target = new TicketThread();
for(int i=0; i<3; i++) {
new Thread(target).start(); //3个线程共同作用一个target
}
}
}
13.9.4 ReentrantReadWriteLock
package game;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class bb {
public static void main(String[] args) {
My2 target = new My2();
Thread t = new Thread(target, "1号窗口:");
Thread t2 = new Thread(target, "2号窗口:");
Thread t3 = new Thread(target, "3号窗口:");
Thread t4 = new Thread(target, "4号窗口:");
t.start();
t2.start();
t3.start();
t4.start();
}
}
class My2 implements Runnable {
int sum = 100;
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
@Override
public void run() {
while (true) {
// t t2 t3 t4都要开门,t有钥匙,进来了出去后,t2再开门干活再锁门
// synchronized (this) {
lock.writeLock().lock();
// sum=1时 t t2 t3 t4都进来
try {
if (sum > 0) {
try {
// t t2 t3 t4都睡了
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// t醒了 sum=1时,sum-- = 1,sum=0
// t2醒了 sum=0时,sum-- = 0,sum=-1
// t3醒了 sum=-1时,sum-- = -1,sum=-2
// t4醒了 sum=-2时,sum-- = -2,sum=-3
System.out.println(Thread.currentThread().getName() + sum--);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.writeLock().unlock();//防止死锁,会自动释放,不释放就独占报错了
}
}
}
}
13.9.5 两种方式的区别
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。
13.9.6 自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
While(true) {
synchronized (this) {
if(ticket<=0) {
break;
}
System.out.println(“窗口:” + Thread.currentThread().getName()
+”, 剩余票数:” + ticket-- );
}
13.10 线程创建的其他方式
13.10.1 ExecutorService/Executors
api
ExecutorService
execute(Runnable任务对象) 把任务丢到线程池
Executors 辅助创建线程池的工具类
newFixedThreadPool(5) 最多5个线程的线程池
newCachedThreadPool() 足够多的线程,使任务不必等待
newSingleThreadExecutor() 只有一个线程的线程池
测试
package game;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test_Pool {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);//2个线程的线程池
// pool = Executors.newCachedThreadPool();
// pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
pool.execute(new R1(i));//放入池中,并等待执行,底层会自动反射run()
}
}
}
class R1 implements Runnable {
int i ;
public R1(int i) {
this.i=i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ " : "+i);
}
}
13.10.2 Callable/Future
package game;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Test6 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newSingleThreadExecutor();
C1 c1 = new C1();
//任务c1放入线程池并行执行
Future<Double> future = pool.submit(c1);
Double r = future.get();//获取结果
System.out.println(r);
pool.shutdown();//关闭任务
}
}
class C1 implements Callable<Double> {
@Override
public Double call() throws Exception {
return Math.random();
}
}
13.11 设计模式
13.11.1 概念
单例模式可以说是大多数开发人员在实际中使用最多的,常见的Spring默认创建的bean就是单例模式的。
单例模式有很多好处,比如可节约系统内存空间,控制资源的使用。
其中单例模式最重要的是确保对象只有一个。
简单来说,保证一个类在内存中的对象就一个。
RunTime就是典型的单例设计,我们通过对RunTime类的分析,一窥究竟。
13.11.2 源码剖析
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
RunTime.java
package java.lang;
public class Runtime {
//1、创建静态的全局唯一的对象
private static Runtime currentRuntime = new Runtime();
//2、私有构造方法,不让外部来调用
/** Don't let anyone else instantiate this class */
private Runtime() {}
//3、通过自定义的静态方法获取实例
public static Runtime getRuntime() {
return currentRuntime;
}
}
13.11.3 饿汉式
package cn.tedu.design;
//测试单例设计模式--就是按照一定的开发步骤,按照一定的模板进行开发,达到程序中只会有一个实例在干活的目的!!
public class Test5_Design {
public static void main(String[] args) {
//4,测试 new多少次都是一个对象???--
// MySingleTon m = new MySingleTon();--构造方法私有化
MySingleTon m1 = MySingleTon.getMy();
MySingleTon m2 = MySingleTon.getMy();
System.out.println(m1==m2);//是同一个对象吗???
System.out.println(m1.equals(m2));//默认用Object的==
}
}
class MySingleTon {
//1,私有化改造方法 -- 目的就是控制外界创建对象的权利
private MySingleTon() {}
//2,封装创建好的对象 -- 封装,不让外界随意看到我创建的对象
static private MySingleTon my = new MySingleTon();
//3,提供公的获取对象的方法
//静态方法,因为没法new了,还想用,就用类名访问--修饰成静态的
static public MySingleTon getMy(){
return my;//静态只能调静态
}
}
13.11.4 懒汉式
//懒汉式 -- 面试重点!!延迟加载思想+线程不安全
class MySingleTon2 {
// 1,私有化改造方法 -- 目的就是控制外界创建对象的权利
private MySingleTon2() {
}
// 2,封装创建好的对象 -- 先不创建对象,啥时候用啥时候创建!!
static private MySingleTon2 my;
// 3,提供公的获取对象的方法
// 静态方法,因为没法new了,还想用,就用类名访问--修饰成静态的
synchronized static public MySingleTon2 getMy() {//b,方法里都是同步代码,可以是同步方法
// synchronized (MySingleTon2.class) {//a,同步代码块,静态方法的锁,是类名.class本类的字节码对象
// t1,t2,t3来准备new
if (my == null) {
// t1 new
// t2 new
// t3 new
my = new MySingleTon2();// 开始创建!! 延迟加载
}
// }
return my;// 静态只能调静态
}
}
13.12 扩展:
13.12.1 日期转换并发异常
SimpleDateFormat类是线程非安全的,多线程并发访问时就会造成异常。怎么证明,我们做过多线程例子轻松就可以证明。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
public static Date parse(String s) {
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
测试方法:
package day12.javapro.threadlocal;
public class TestSimpleDateTime implements Runnable{
@Override
public void run() {
System.out.println(DateUtil.parse(“1997-07-01”));
}
public static void main(String[] args) {
Runnable target = new TestSimpleDateTime();
for(int i=0; i<20; i++) {
new Thread(target).start();
}
}
}
执行出错:
Exception in thread “Thread-4” java.lang.NumberFormatException: For input string: “”
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Long.parseLong(Unknown Source)
at java.lang.Long.parseLong(Unknown Source)
at java.text.DigitList.getLong(Unknown Source)
at java.text.DecimalFormat.parse(Unknown Source)
at java.text.SimpleDateFormat.subParse(Unknown Source)
at java.text.SimpleDateFormat.parse(Unknown Source)
at java.text.DateFormat.parse(Unknown Source)
at day12.javapro.threadlocal.DateUtils.parse(DateUtils.java:11)
at day12.javapro.threadlocal.TestDateThread.run(TestDateThread.java:10)
at java.lang.Thread.run(Unknown Source)
Tue Jul 01 00:00:00 CST 1997
Tue Jul 01 00:00:00 CST 1997
Tue Jul 01 00:00:00 CST 1997
由上面我们证明了SimpleDateFormat类的确是线程非安全的,多线程并发访问时这个转换还未完成,那个转换就开始了,导致转换参数错误,从而结果错误。
13.12.2 解决方案1
我们分析下,上面什么原因造成的呢?我们访问的是成员变量,成员变量在高并发下就会产生线程安全问题。那我们说过成员变量共享引起的错误最简单的解决方案就是不共享。那我们就改造成员变量为方法内私有变量,这样每次都new创建新的对象。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
public static Date parse(String s) {
SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
问题是解决了,但每次都new创建新的对象,这样内存空间浪费比较大。显然不是最佳的解决方案。
13.12.3 解决方案2
使用同步锁,但程序显然会阻塞,无法并发执行,工作效率低。
Package day12.javapro.threadlocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
synchronized public static Date parse(String s) {
try {
return sdf.parse(s);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
13.12.4 解决方案3
用jdk1.8中的日期格式类DateFormatter,DateTimeFormatter,但这就需要改动现有项目中所有用到这个工具类的代码,工作量较大。而且API写的很烂,不好用,暂时不推荐使用。
13.12.5 解决方案4
用ThreadLocal,一个线程一个SimpleDateFormat对象
工具类:
package day12.javapro.threadlocal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
private static ThreadLocal<DateFormat> threadlocal = new ThreadLocal<DateFormat>() {
protected DateFormat initialValue() {
return new SimpleDateFormat(“yyyy-MM-dd”);
};
};
public static Date parse(String s) throws ParseException {
return threadlocal.get().parse(s);
}
}
测试类:
package day12.javapro.threadlocal;
import java.text.ParseException;
public class TestDate implements Runnable{
@Override
public void run() {
try {
System.out.println( DateUtils.parse(“2018-07-09”) );
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Runnable target = new TestDate();
for(int i=0;i<10;i++) {
new Thread(target).start();
}
}
}
14 注解+反射
14.1 注解
14.1.1 概念
注解很厉害,它可以增强我们的java代码,同时利用反射技术可以扩充实现很多功能。它们被广泛应用于三大框架底层。传统我们通过xml文本文件声明方式,而现在最主流的开发都是基于注解方式,代码量少,框架可以根据注解去自动生成很多代码,从而减少代码量,程序更易读。例如最火爆的SpringBoot就完全基于注解技术实现。
注解设计非常精巧,初学时觉得很另类甚至多余,甚至垃圾。有了java代码干嘛还要有@注解呢?但熟练之后你会赞叹,它竟然可以超越java代码的功能,让java代码瞬间变得强大。大家慢慢体会吧。
常见的元注解:@Target、@Retention,jdk提供将来描述我们自定义的注解的注解。听起来好绕,别着急,做两个例子,立刻清晰。现在现有“元注解”这个概念。
14.1.2 分类
l JDK自带注解
l 元注解
l 自定义注解
14.1.3 JDK注解
JDK注解的注解,就5个:
l @Override
l @Deprecated标记就表明这个方法已经过时了,但我就要用,别提示我过期
l @SuppressWarnings(“deprecation”) 忽略警告
l @SafeVarargs jdk1.7出现,堆污染,不常用
l @FunctionallInterface jdk1.8出现,配合函数式编程拉姆达表达式,不常用
14.1.4 元注解
描述注解的注解,就5个:
l @Target 注解用在哪里:类上、方法上、属性上
l @Retention 注解的生命周期:源文件中、class文件中、运行中
l @Inherited 允许子注解继承
l @Documented 生成javadoc时会包含注解,不常用
l @Repeatable注解为可重复类型注解,可以在同一个地方多次使用,不常用
14.2 元注解
14.2.1 @Target ElementType.class
描述注解的使用范围:
l ElementType.ANNOTATION_TYPE 应用于注释类型
l ElementType.CONSTRUCTOR 应用于构造函数
l ElementType.FIELD 应用于字段或属性
l ElementType.LOCAL_VARIABLE 应用于局部变量
l ElementType.METHOD 应用于方法级
l ElementType.PACKAGE 应用于包声明
l ElementType.PARAMETER 应用于方法的参数
l ElementType.TYPE 应用于类的元素
14.2.2 @Retention RetentionPolicy.class
定义了该注解被保留的时间长短,某些注解仅出现在源代码中,而被编译器丢弃;
而另一些却被编译在class文件中; 编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。
为何要分有没有呢?没有时,反射就拿不到,从而就无法去识别处理。
l SOURCE 在源文件中有效(即源文件保留)
l CLASS 在class文件中有效(即class保留)
l RUNTIME 在运行时有效(即运行时保留)
14.3 自定义注解
14.3.1 定义注解
//1,定义注解
//1.1,设置注解的使用范围@Target,啥都不写,哪儿都能用
//@Target({ElementType.METHOD})//作用于方法上
//@Target({ElementType.FIELD})//作用于属性上
@Target({ElementType.METHOD , ElementType.PACKAGE})//作用范围
@Retention(RetentionPolicy.SOURCE)//生命周期
@Target({ElementType.TYPE})//作用于类上
@interface Test{
//3,定义属性
int age() default 0;//使用时,必须给age属性赋值,如:age=X。除非设置好默认值。
//()不是参数,也不能写参数,只是特殊语法
//4,特殊属性value
String value() default "";//使用时,必须给value属性赋值,如:X | value=X。除非设置好默认值
}
注意:注解的语法写法和常规java的语法写法不同
14.3.2 使用注解
//2,使用注解
//@Test
//5,注解的组合属性
@Test(value="",age=0)
class HelloTest{
// @Test(value="",age=0)
String name;
}
14.3.3 解析注解
判断注解是否存在
package javapro.spring.annotation;
public class TestAnnotation {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("javapro.spring.annotation.HelloController");
Controller c = clazz.getAnnotation(Controller.class);
if( c != null) {
System.out.println(c);
System.out.println(c.value());
}
}
14.4 反射
14.4.1 概念
Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,也有称作“自省”。反射非常强大,它甚至能直接操作程序的私有属性。我们前面学习都有一个概念,private的只能类内部访问,外部是不行的,但这个规定被反射赤裸裸的打破了。
反射就像一面镜子,它可以在运行时获取一个类的所有信息,可以获取到任何定义的信息(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。
14.4.2 为什么需要反射
好好的我们new User(); 不是很好,为什么要去通过反射创建对象呢?
那我要问你个问题了,你为什么要去餐馆吃饭呢?
例如:我们要吃个牛排大餐,如果我们自己创建,就什么都得管理。
好处是,每一步做什么我都很清晰,坏处是什么都得自己实现,那不是累死了。牛接生你管,吃什么你管,屠宰你管,运输你管,冷藏你管,烹饪你管,上桌你管。就拿做菜来说,你能有特级厨师做的好?
那怎么办呢?有句话说的好,专业的事情交给专业的人做,饲养交给农场主,屠宰交给刽子手,烹饪交给特级厨师。那我们干嘛呢?
我们翘起二郎腿直接拿过来吃就好了。
再者,饭店把东西做好,不能扔到地上,我们去捡着吃吧,那不是都成原始人了。那怎么办呢?很简单,把做好的东西放在一个容器中吧,如把牛排放在盘子里。
在开发的世界里,spring就是专业的组织,它来帮我们创建对象,管理对象。我们不在new对象,而直接从spring提供的容器中beans获取即可。Beans底层其实就是一个Map<String,Object>,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。
总结一句,类不是你创建的,是你同事或者直接是第三方公司,此时你要或得这个类的底层功能调用,就需要反射技术实现。有点抽象,别着急,我们做个案例,你就立马清晰。
14.4.3 反射Class类对象
Class.forName(“类的全路径”);
类名.class
对象.getClass();
14.4.4 常用方法
获得包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
!!成员变量定义信息
getFields()//获得所有公开的成员变量,包括继承的变量
getDeclaredFields()//获得本类定义的成员变量,包括私有,不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
!!构造方法定义信息
getConstructor(参数类型列表)//获得公开的构造方法
getConstructors()//获得所有公开的构造方法
getDeclaredConstructors()//获得所有构造方法,包括私有
getDeclaredConstructor(int.class, String.class)
方法定义信息
getMethods()//获得所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获得本类定义的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名, int.class, String.class)
反射新建实例
c.newInstance();//执行无参构造
c.newInstance(6, “abc”);//执行有参构造
c.getConstructor(int.class, String.class); //执行含参构造,获取构造方法
反射调用成员变量
c.getDeclaredField(变量名); //获取变量
c.setAccessible(true); //使私有成员允许访问
f.set(实例, 值); //为指定实例的变量赋值,静态变量,第一参数给 null
f.get(实例); //访问指定实例的变量的值,静态变量,第一参数给 null
反射调用成员方法
获取方法
Method m = c.getDeclaredMethod(方法名, 参数类型列表);
m.setAccessible(true) ;//使私有方法允许被调用
m.invoke(实例, 参数数据) ;//让指定的实例来执行该方法
14.5 反射的应用
14.5.1 创建类
class Student{
String name="jack";
int age=20;
public Student() {
System.out.println("无参构造");
}
public Student(String name) {
this.name=name;
System.out.println("含参构造"+name);
}
public void show(int a) {
System.out.println("show()..."+a);
}
}
14.5.2 获取类对象
private static void method() throws Exception {
Class clazz = Student.class;
Class<?> clazz2 = Class.forName("seday15.Student");
Class clazz3 = new Student().getClass();
System.out.println(clazz.getName());
System.out.println(clazz2.getName());
System.out.println(clazz3.getName());
}
14.5.3 获取构造方法
private static void method3(Class clazz) {
Constructor[] cs = clazz.getDeclaredConstructors();
for (Constructor c : cs) {
String name = clazz.getSimpleName();
System.out.println(name);
Class[] cs2 = c.getParameterTypes();//参数
System.out.println(Arrays.toString(cs2));
}
}
14.5.4 获取成员方法
private static void method4(Class clazz) {
Method[] ms = clazz.getMethods();
for (Method m : ms) {
String name = m.getName();
System.out.println(name);
Class<?>[] cs = m.getParameterTypes();
System.out.println(Arrays.toString(cs));
}
}
14.5.5 获取成员变量
private static void method2(Class clazz) {
Field[] fs = clazz.getFields();//获取public的属性
for (Field f : fs) {
String name = f.getName();
String tname = f.getType().getSimpleName();
System.out.println(name);
System.out.println(tname);
}
}
14.5.6 创建对象
package seday15;
import java.lang.reflect.Constructor;
import java.util.Scanner;
//反射新建两个对象
public class Test3 {
public static void main(String[] args) throws Exception {
String s = new Scanner(System.in).nextLine();
Class<?> clazz = Class.forName(s);
Object o1 = clazz.newInstance();//用无参构造
System.out.println(o1);
Constructor<?> c = clazz.getConstructor(String.class);//用含参构造
Object o2 = c.newInstance("jack");
System.out.println(o2);
}
}
14.5.7 练习:熟悉API
创建Teacher.java
package seday16new;
public class Teacher {
public String name="jack";
int age = 20;
public Teacher() { }
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
public void show() {
System.out.println("Teacher.show()");
}
@Override
public String toString() {
return "Teacher [name=" + name + ", age=" + age + "]";
}
}
测试
package seday16new;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test3_Reflect2 {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("seday16new.Teacher");
// method(clazz);//方法
// method2(clazz);//属性
method3(clazz);//构造
}
private static void method3(Class<?> clazz) throws Exception {
Constructor c = clazz.getConstructor(null);//无参构造
System.out.println(c.newInstance(null));
Constructor<?> cc = clazz.getConstructor(String.class,int.class);//含参
System.out.println(cc.newInstance("rose",10));
}
private static void method2(Class<?> clazz) throws Exception {
Field f = clazz.getField("name");//要求属性必须是public的
Field f2 = clazz.getField("age");// java.lang.NoSuchFieldException
System.out.println(f.getType().getName());
System.out.println(f2.getType().getName());
}
private static void method(Class clazz) throws Exception {
Method m = clazz.getMethod("show", null);
Object obj = clazz.newInstance();
m.invoke(obj);//执行方法
}
}
14.6 暴力反射
指可以将程序中的私有的属性或者方法通过反射技术,暴力的获取到资源。需要使用的常见方法如下:
14.6.1 创建Person类
class Person{
private String name="jack";
private int age = 30;
private void show(int[] a) {
System.out.println("show()..."+Arrays.toString(a));
}
private void test() {
System.out.println("test()...");
}
}
14.6.2 测试
1、 获取私有属性值并修改
2、 获取私有方法并执行
package seday16new;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test3_ReflectPerson {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("seday16new.Person");
// method(clazz);//隐私属性
method2(clazz);//执行方法
}
private static void method2(Class<?> clazz) throws Exception {
Method m = clazz.getDeclaredMethod("show", int[].class);
Object obj = clazz.newInstance();
m.setAccessible(true);//方法隐私可见
m.invoke(obj, new int[]{1,2,3});//执行
}
private static void method(Class clazz) throws Exception {
Field f = clazz.getDeclaredField("name");
System.out.println(f.getType().getName());
f.setAccessible(true);//属性隐私可见
Object obj = clazz.newInstance();
// f.set(obj, "rose");//设置值
System.out.println(f.get(obj));//获取值
//---所有属性
Field[] fs = clazz.getDeclaredFields();
for (Field ff : fs) {
System.out.println(ff);
ff.setAccessible(true);//暴力反射
System.out.println(ff.get(obj));
}
}
}
15 socket
15.1 内部类
15.1.1 概述
如果一个类存在的意义就是为指定的另一个类,可以把这个类放入另一个类的内部。就是把类定义在类的内部的情况就可以形成内部类的形式。
A类中又定义了B类,B类就是内部类。B类可以当做A类的一个成员看待。
15.1.2 特点
1、 内部类可以直接访问外部类中的成员,包括私有成员
2、 外部类要访问内部类的成员,必须要建立内部类的对象
3、 在成员位置的内部类是成员内部类
4、 在局部位置的内部类是局部内部类
15.1.3 成员内部类
被private修饰
package cn.xxx.inner;
//测试内部类被private修饰
public class Test5_InnerClass2 {
public static void main(String[] args) {
//TODO 创建内部类对象,并执行show()
// Outer2.Inner2 oi = new Outer2().new Inner2();//报错,Inner2已经被private了
//3,测试被private的内部类的资源能否执行!
new Outer2().test();
}
}
class Outer2{
//2,如果想要访问private的内部类,可以访问外部类提供的对应方法
public void test() {
//访问内部类方法
new Inner2().show();
}
//位置在类里方法外--成员内部类
//1,内部类可以被private修饰,但是外界无法直接创建对象了!
private class Inner2{
public void show() {
System.out.println("Inner2.show()");
}
}
}
被static修饰
package cn.tedu.inner;
//测试内部类被static修饰
public class Test6_InnerClass3 {
public static void main(String[] args) {
// 创建内部类对象测试show()
// Outer3.Inner3 oi = new Outer3().new Inner3();//报错,原因是Inner3是静态的内部类
Outer3.Inner3 oi = new Outer3.Inner3();//Outer3.Inner3通过类名.调用类中的静态资源
oi.show();
Outer3.Inner3.show2();//调用静态内部类里的静态方法
}
}
class Outer3{
//1,内部类被static修饰--随着类的加载而加载,会造成内存资源浪费,并不常用!
static class Inner3{
public void show() {
System.out.println("Inner3.show()");
}
static public void show2() {
System.out.println("Inner3.show2()");
}
}
}
15.1.4 局部内部类
package cn.xxx.inner;
//测试局部内部类
public class Test7_InnerClass4 {
public static void main(String[] args) {
//创建对象测试test()的执行
Outer4 out = new Outer4();
out.show();
}
}
class Outer4{
public void show() {
//位置如果在方法里--局部内部类--减少类的作用范围,提高效率节省内存,不常见!
class Inner4{
public void test() {
System.out.println("Inner4.test()");
}
}
//触发内部类的功能
new Inner4().test();
}
}
15.1.5 匿名内部类
匿名内部类属于局部内部类,并且是没有名字的内部类。
package cn.tedu.inner;
//测试匿名内部类
public class Test8_InnerClass5 {
public static void main(String[] args) {
new Hello() {// 匿名对象,本身接口不能new,这里new Hello()匿名对象,就相当于Hello接口的实现类
// 匿名内部类
@Override
public void save() {
System.out.println("save()..");
}
@Override
public void update() {
System.out.println("update()..");
}
}.update();// 触发指定的方法
new Hello2() {//抽象类的匿名内部类
@Override
public void show() { }
}.show();
new Animal() {//普通类的匿名内部类
@Override
public void eat() { }
};
}
}
//TODO 创建匿名对象+匿名内部类测试
class Animal{
public void eat() {}
}
abstract class Hello2 {
abstract public void show();
public void delete() { }
}
// 定义接口
interface Hello {
void save();
void update();
}
15.2 网络
查看本机ip地址
15.3 Socket
15.3.1 概述
也叫套接字编程,是一个抽象层。
应用程序可以通过它发送或接收数据,可对其像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口与协议的组合。
Socket就是为网络编程提供的一种机制 / 通信的两端都有Socket
网络通信其实就是Socket间的通信 / 数据在两个Socket间通过IO传输
15.4 服务器端-ServerSocket
在服务器端,选择一个端口号,在指定端口上等待客户端发起连接。
启动服务:ServerSocket ss = new ServerSocket(端口);
等待客户端发起连接,并建立连接通道:Sokcet socket = ss.accept();
15.5 客户端-Socket
新建Socket对象,连接指定ip的服务器的指定端口
Socket s = new Socket(ip, port);
从Socket获取双向的流
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
15.6 入门案例
服务器端接收客户端发来的hello,并给客户端响应hello。
15.6.1 服务器端
说明其中,server端的accept()是阻塞的,客户端不连接,服务器不执行后面流程。
in.read()也是阻塞的,读不到就死等。
package seday16;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
//1,在指定端口启动服务
ServerSocket server = new ServerSocket(8000);
//2,等待客户端发起连接,并建立通道
Socket socket = server.accept();
//3,取出双向的流
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
//4,通信
/*
* 通信协议:
* 1,通信流程
* 2,数据格式
* 先接收hello,在发送world
*/
//接收客户端发来的hello
for(int i = 0 ; i < 5 ;i++) {
//一个一个字节从网络流中读取客户端发来的数据
char c = (char) in.read();
System.out.print(c);//一行展示收到的数据
}
//给客户端发送数据
out.write("world".getBytes());
out.flush();//刷出内存缓存
//释放资源
socket.close();//断开连接
server.close();//释放端口
}
}
15.6.2 客户端
package seday16;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception, IOException {
//1,与服务器建立连接
//同桌启动服务器,你当客户端发送
Socket s = new Socket("127.0.0.1",8000);
//2,取出双向的流
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
//3,通信
/*
* 先发送Hello,再接收world
*/
//给服务器发送数据
out.write("hello".getBytes());
out.flush();
//接收服务器响应的数据
for (int i = 0; i < 5 ; i++) {
char c = (char) in.read();
System.out.print(c);//同一行展示
}
//释放资源
s.close();
}
}
更多推荐
所有评论(0)