Java入门笔记补充
数据类型
数据类型
整形拓展
二进制整数,如 : 0b0101(JDK1.7)
十进制整数,如:99, -500, 0。
八进制整数,要求以 0 开头,如:015。
十六进制数,要求 0x 或 0X 开头,如:0x15 。
浮点型拓展
别使用浮点数比较,要用的话用Decimal类
【金融面试问:银行金融业务用什么类型表示?】
浮点类型float, double的数据不适合在不容许舍入误差的金融计算领域。
如果需要进行不产生舍入误差的精确数字计算,需要使用BigDecimal
类。
主要理由:
由于字长有限,浮点数能够精确表示的数是有限的,因而也是离散的。浮点数一般都存在舍入误差,很
多数字无法精确表示,其结果只能是接近,但不等于;二进制浮点数不能精确的表示0.1,0.01,0.001这样10的负次幂。并不是所有的小数都能可以精确的用二进制浮点数表示。
最好完全避免使用浮点数比较 !
大数值:Java.math下面的两个有用的类:BigInteger和BigDecimal,这两个类可以处理任意长度的数
值。BigInteger实现了任意精度的整数运算。BigDecimal实现了任意精度的浮点运算。
浮点数使用总结:
- 默认是double
- 浮点数存在舍入误差,很多数字不能精确表示。如果需要进行不产生舍入误差的精确数字计算,需
要使用BigDecimal类。 - 避免比较中使用浮点数
字符型拓展
-
ASCIIS码:
1个英文字母(不分大小写)= 1个字节的空间
1个中文汉字 = 2个字节的空间
1个ASCII码 = 一个字节 -
UTF-8编码:
英文 = 1个字节
中文 = 3个字节 -
Unicode编码:
英文 = 2个字节
中文 = 2个字节
类型转换
浮点数到整数的转换是通过舍弃小数
得到,而不是四舍五入
,例如:
(int)23.7 == 23;
(int)-45.89f == -45
变量
变量作用域
变量根据作用域可划分为三种:
- 类变量(
静态变量
: static variable):独立于方法之外的变量,用 static 修饰。 - 实例变量(
成员变量
:member variable):独立于方法之外的变量,不过没有 static 修饰。 局部变量
(local variable):类的方法中的变量。
局部变量
在使用前必须先声明和初始化
(赋初值)。
局部变量没有默认值,所以局部变量被声明后,必须经过初始化,才可以使用。
实例变量
自动初始化成该类型的默认初始值
静态变量
自动初始化成该类型的默认初始值
变量的命名规范
- 所有变量、方法、类名:见名知意
- 类成员变量:首字母小写和驼峰原则 : monthSalary
- 局部变量:首字母小写和驼峰原则
- 常量:大写字母和下划线:MAX_VALUE
- 类名:
首字母大写
和驼峰原则: Man, GoodMan - 方法名:
首字母小写
和驼峰原则: run(), runRun()
数组
三种初始化
静态初始化
除了用new关键字来产生数组以外,还可以直接在定义数组的同时就为数组元素分配空间并赋值。
int[] a = {1,2,3};
Man[] mans = {new Man(1,1),new Man(2,2)};
动态初始化
数组定义、为数组元素分配空间、赋值的操作、分开进行。
int[] a = new int[2];
a[0]=1;
a[1]=2;
数组的默认初始化
数组是引用类型,它的元素相当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被隐式初始化。
public static void main(String[] args) {
int[] a=new int[2];
boolean[] b = new boolean[2];
String[] s = new String[2];
System.out.println(a[0]+":"+a[1]); //0,0
System.out.println(b[0]+":"+b[1]); //false,false
System.out.println(s[0]+":"+s[1]); //null, null
}
Arrays类
具有以下常用功能:
- 给数组赋值:通过 fill 方法。
- 对数组排序:通过 sort 方法,按升序。
- 比较数组:通过 equals 方法比较数组中元素值是否相等。
- 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。
1、打印数组
public static void main(String[] args) {
int[] a = {1,2};
System.out.println(a); //[I@1b6d3586
System.out.println(Arrays.toString(a)); //[1, 2]
}
2、数组排序
public static void main(String[] args) {
int[] a = {1,2,323,23,543,12,59};
System.out.println(Arrays.toString(a));
Arrays.sort(a);
System.out.println(Arrays.toString(a));
}
3、二分法查找
在数组中查找指定元素并返回其下标
注意:使用二分搜索法来搜索指定的数组,以获得指定的值。必须在进行此调用之前对数组进行排序
(通过sort方法等)。如果没有对数组进行排序,则结果是不确定的。
如果数组包含多个带有指定值的元素,则无法保证找到的是哪一个
public static void main(String[] args) {
int[] a = {1,2,323,23,543,12,59};
Arrays.sort(a); //使用二分法查找,必须先对数组进行排序
System.out.println("该元素的索引:"+Arrays.binarySearch(a, 12));
}
4、元素填充
public static void main(String[] args) {
int[] a = {1,2,323,23,543,12,59};
Arrays.sort(a); //使用二分法查找,必须先对数组进行排序
Arrays.fill(a, 2, 4, 100); //将2到4索引的元素替换为100
System.out.println(Arrays.toString(a));
}
5、数组转换为List集合
int[] a = {3,5,1,9,7};
List<int[]> list = Arrays.asList(a);
内存分析
栈 stack:
- 每个线程私有,不能实现线程间的共享!
- 局部变量放置于栈中。
栈是由系统自动分配,速度快!栈是一个连续的内存空间!
堆 heap:
- 放置new出来的对象!
- 堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(也是堆):
- 被所有线程共享!
- 用来存放程序中永远是不变或唯一的内容。(类代码信息、静态变量、
字符串常量池
)
封装继承多态
继承
private 构造器 不行
父类中的属性和方法使用private
修饰,在子类中继承后."不可以直接"使用(继承过去了但是无法使用)
static final
修饰的属性和方法也会被继承
super this
父类中的构造器是不能被子类继承的
,但是子类的构造器中,会隐式的调用父类中的无参构造器(默认使用super关键字)。
super() 和 this() 都需要是构造函数的第一个语句
super 和 this 不能够同时调用构造方法。(因为this也是在构造方法的第一个语句)
override
static private final
方法 不行
-
static静态方法不能重写
-
父类的静态方法不能被子类重写为非静态方法 //编译出错
-
父类的非静态方法不能被子类重写为静态方法;//编译出错
-
子类可以定义与父类的静态方法同名的静态方法(但是这个不是覆盖)
-
私有方法不能被子类重写
,子类继承父类后,是不能直接访问父类中的私有方法
的,那么就更谈不上重写了。
public class Person{
private void run(){}
}
//编译通过,但这不是重写,只是俩个类中分别有自己的私有方法
public class Student extends Person{
private void run(){}
}
重写的语法
- 必须存在继承关系。
- 方法名和形式参数 必须跟父类是一致的。
- 子类的
权限修饰符
必须要大于
或者等于父类的权限修饰符。( private < protected < public,friendly < public )- 子类的
返回值类型
必须小于
或者等于父类的返回值类型。( 子类 < 父类 ) 数据类型没有明确的上下级关系- 方法重写的时候,子类的
异常类型
要小于
或者等于父类的异常类型。
多态
能override才能多态
static private protected final
不行
多态存在必须要有“子类重写父类方法”这一条件,那么以下三种类型的方法是没
有办法表现出多态特性的(因为不能被重写):
static
方法,因为被static修饰的方法是属于类的,而不是属于实例的final
方法,因为被final修饰的方法无法被子类重写- private方法和protected方法,前者是因为被private修饰的方法对子类不可见,后者是因为尽管被protected修饰的方法可以被子类见到,也可以被子类重写,但是它是无法被外部所引用的,一个不能被外部引用的方法,怎么能谈多态呢
引用类型转换
-
把子类对象直接赋给父类引用叫upcasting向上转型,向上转型不用强制转型。
-
把指向子类对象的父类引用赋给子类引用叫向下转型(downcasting),要强制转型。
-
upcasting 会丢失子类特有的方法,但是子类
overriding
父类的方法,子类方法有效
修饰符
static
1、创建和初始化对象的过程
Person s = new Student();
总体原则:先静态后成员 先属性后父类构造器后代码块后构造器
【Student类之前没有进行类加载】
- 类加载,同时初始化类中
静态的属性
- 执行
静态代码块
分配内存空间
,同时初始化非静态的属性
(赋默认值,0/false/null)- 调用Student的
父类构造器
- 对Student中的属性进行显示赋值(如果有的话)
- 执行
匿名代码块
- 执行
构造器
- 返回内存地址
注:子类中非静态属性的显示赋值是在父类构造器执行完之后和子类中的匿名代码块执行之前的时候
public class Person{
private String name = "zs";
public Person() {
System.out.println("Person构造器"); //2
print();
}
public void print(){
System.out.println("Person print方法: name = "+name);
}
}
public class Student extends Person{
private String name = "tom";
{
System.out.println("Student匿名代码块"); //3
}
static{
System.out.println("Student静态代码块"); //1
}
public Student(){
System.out.println("Student构造器"); //4
}
public void print(){
System.out.println("student print方法: name = "+name);
}
public static void main(String[] args) {
new Student();
}
}
//输出:
Student静态代码块
Person构造器
student print方法: name = null
Student匿名代码块
Student构造器
Student s = new Student();
Student类之前已经进行了类加载
1.分配内存空间,同时初始化非静态的属性(赋默认值,0/false/null)
2.调用Student的父类构造器
3.对Student中的属性进行显示赋值(如果有的话)
4.执行匿名代码块
5.执行构造器
6.返回内存地址
final
1、修饰类
用final修饰的类不能被继承,没有子类
2、修饰方法
用final修饰的方法可以被继承,但是不能被子类的重写
3、修饰变量
用final修饰的变量表示常量,只能被赋一次值.其实使用final修饰的变量也就成了常量了,因为值不会再变了。
abstract
思考1 : 抽象类不能new对象,那么抽象类中有没有构造器?
抽象类是不能被实例化,有构造器,抽象类的目的就是为实现多态中的共同点,抽象类的构造器会在子类实例化时调用,因此它也是用来实现多态中的共同点构造,不建议这样使用!
抽象类中可以没有抽象方法,也可以包含非抽象方法,但有抽象方法的类一定是抽象类。
接口
接口与抽象类的区别
抽象类也是类,除了可以写抽象方法以及不能直接new对象之外,其他的和普通类没有什么不一样的。接口已经另一种类型了,和类是有本质的区别的,所以不能用类的标准去衡量接口。
声明类的关键字是class,声明接口的关键字是interface。
抽象类是用来被继承的,java中的类是单继承。
类A继承了抽象类B,那么类A的对象就属于B类型了,可以使用多态
一个父类的引用,可以指向这个父类的任意子类对象
注:继承的关键字是extends
接口是用来被类实现的,java中的接口可以被多实现。
类A实现接口B、C、D、E…,那么类A的对象就属于B、C、D、E等类型了,可以使用多态
一个接口的引用,可以指向这个接口的任意实现类对象
注:实现的关键字是implements
接口中的方法都是抽象方法
接口中可以不写任何方法,但如果写方法了,该方法必须是抽象方法
接口中的变量都是静态常量(public static final修饰)
接口中可以不写任何属性,但如果写属性了,该属性必须是public static final修饰的静态常量。
注:可以直接使用接口名访问其属性。因为是public static修饰的
注:声明的同时就必须赋值
.(因为接口中不能编写静态代码块)
public interface Action{
public static final String NAME = "tom";
//默认就是public static final修饰的
int AGE = 20;
}
main:
System.out.println(Action.NAME);
System.out.println(Action.AGE);
总结
1、Java接口中的成员变量默认都是public,static,final类型的(都可省略),必须被显示初始化
,即接
口中的成员变量为常量
(大写,单词之间用"_"分隔)
2、Java接口中的方法默认都是public,abstract类型的(都可省略),没有方法体,不能被实例化
3、Java接口中只能包含public,static,final类型的成员变量和public,abstract类型的成员方法
4、接口中没有构造方法
,不能被实例化
5、一个接口不能实现(implements)另一个接口,但它可以继承多个其它的接口
6、Java接口必须通过类来实现它的抽象方法
7、当类实现了某个Java接口时,它必须实现接口中的所有
抽象方法,否则这个类必须声明为抽象
类
8、不允许创建接口的实例(实例化),但允许定义接口类型的引用变量,该引用变量引用实现了这
个接口的类的实例
9、 一个类只能继承一个直接的父类,但可以实现多个接口,间接的实现了多继承.
异常
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在
编译时不能被简单地忽略
。 - 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,
运行时异常可以在编译时被忽略
。 - 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在
编译也检查不到
的。
异常体系结构
Java把异常当作对象来处理,并定义一个基类java.lang.Throwable 作为所有异常的超类。
在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error
和异常Exception
。
异常之间的区别与联系
1、Error
2、Exception
3、检查异常和不受检查异常
集合
重中之重
List
ArrayList
`
1、ArrayList概述
- ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。
- 该类封装了一个动态再分配的Object[]数组,每一个类对象都有一个capacity【容量】属性,表示它们所封装的Object[]数组的长度,当向ArrayList中添加元素时,该属性值会自动增加。如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。
- ArrayList的用法和Vector相类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。另外,ArrayList和Vector的区别是:
ArrayList是线程不安全的
,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的
。
ArrayList和Collection的关系:
2、ArrayList的数据结构
说明:底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。我们对ArrayList类的实例的所有的操作底层都是基于数组的。
3、ArrayList源码分析
1、继承和层次关系
CTRL + H 查看继承关系
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
我们看一下ArrayList的继承结构:
ArrayList extends AbstractList
AbstractList extends AbstractCollection
所有类都继承Object 所以ArrayList的继承结构就是上图这样。
【分析】
-
为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类,应该就是这个作用。 -
ArrayList实现了哪些接口?
List接口:我们会出现这样一个疑问,在查看了ArrayList的父类 AbstractList也实现了List接口,那为什么子类ArrayList还是去实现一遍呢?
这是想不通的地方,所以我就去查资料,有的人说是为了查看代码方便,使观看者一目了然,说法不一,但每一个让我感觉合理的,但是在stackOverFlow中找到了答案,这里其实很有趣。
开发这个collection 的作者Josh说:
这其实是一个mistake[失误],因为他写这代码的时候觉得这个会有用处,但是其实并没什么用,但因为没什么影响,就一直留到了现在。
**RandomAccess接口:**这个是一个标记性接口
,通过查看api文档,它的作用就是用来快速随机存取,有关效率的问题,在实现了该接口的话
,那么使用普通的for循环来遍历,性能更高
,例如ArrayList。而没有实现该接口的话
,使用Iterator来迭代,这样性能更高
,例如linkedList。所以这个标记性只是为了让我们知道我们用什么样的方式去获取数据性能更好。
Cloneable接口:实现了该接口,就可以使用Object.Clone()方法了。
Serializable接口:实现该序列化接口,表明该类可以被序列化.
2、类中的属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//版本号
private static final long serialVersionUID = 8683452581122892189L;
//Default initial capacity. 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
//Shared empty array instance used for empty instances.
//空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//缺省空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组 transient表示不序列化
transient Object[] elementData;
// 实际元素大小,默认为0
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
3、构造方法
-
无参构造方法
/* Constructs an empty list with an initial capacity of ten. 这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10. */ //ArrayList中储存数据的其实就是一个数组,这个数组就是elementData. public ArrayList() { super(); //调用父类中的无参构造方法,父类中的是个空的构造方法 this.elementData = EMPTY_ELEMENTDATA; //EMPTY_ELEMENTDATA:是个空的Object[], 将elementData初始化,elementData也是个Object[]类型。空的Object[]会给默认大小10,等会会解释什么时候赋值的。 }
-
有参构造方法 1
/*
Constructs an empty list with the specified initial capacity.
构造具有指定初始容量的空列表。
@param initialCapacity the initial capacity of the list
初始容量列表的初始容量
@throws IllegalArgumentException if the specified initial capacity isnegative
如果指定的初始容量为负,则为IllegalArgumentException
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//将自定义的容量大小当成初始化 initialCapacity 的大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; //等同于无参构造方法
} else {
//判断如果自定义大小的容量小于0,则报下面这个非法数据异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
- 有参构造方法 2
/*
Constructs a list containing the elements of the specified collection,in the order they are returned by the collection's iterator.
按照集合迭代器返回元素的顺序构造包含指定集合的元素的列表。
@param c the collection whose elements are to be placed into this list
@throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); //转换为数组
//每个集合的toarray()的实现方法不一样,所以需要判断一下,如果不是Object[].class类型,那么就需要使用ArrayList中的方法去改造一下。
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
这个构造方法不常用,举个例子就能明白什么意思
举个例子: Strdent exends Person , ArrayList、 Person这里就是泛型 , 我还有一个Collection,由于这个Student继承了Person,那么根据这个构造方法,我就可以把这个Collection转换为ArrayList, 这就是这个构造方法的作用 。
【总结】ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实本质上就是一个数组,在其中就叫elementData。
4、核心方法-add
/**
Appends the specified element to the end of this list.
将指定元素追加到此列表的末尾
Params:
e – element to be appended to this list
Returns:
true (as specified by Collection.add)
*/
public boolean add(E e) {
//确定内部容量是否够了,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; //在数据中正确的位置上放上元素e,并且size++
return true;
}
【分析:ensureCapacityInternal(xxx); 确定内部容量的方法】
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//判断初始化的elementData是不是空的数组,也就是没有长度
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是在这里,还没有真正的初始化这个elementData的大小。
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用
return minCapacity;
}
//arrayList核心的方法,能扩展数组大小的真正秘密。
private void grow(int minCapacity) {
// overflow-conscious code
//将扩充前的elementData大小给oldCapacity
int oldCapacity = elementData.length;
//newCapacity就是1.5倍的oldCapacity
int newCapacity = oldCapacity + (oldCapacity >> 1);
//这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//新的容量大小已经确定好了,就copy数组,改变容量大小咯。
elementData = Arrays.copyOf(elementData, newCapacity);
}
总结
正常情况下会扩容1.5倍
,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值容量值。
当我们调用add方法时,实际上的函数调用如下:
4、总结
1)arrayList可以存放null。
2)arrayList本质上就是一个elementData数组。
3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法。
4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而
clear是全是删除集合中的元素。
5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很
多,有移动很多数据才能达到应有的效果
6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环
。
LinkedList
插入,删除操作频繁时,可使用LinkedList来提高效率。
LinkedList提供对头部和尾部元素进行添加和删除操作的方法!
1、LinkedList概述
LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表
实现
的。
LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队
列进行操作。
LinkedList 实现 List 接口,能对它进行队列操作。
LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
LinkedList 是非同步的。
2、LinkedList的数据结构
如上图所示,LinkedList底层使用的双向链表结构,有一个头结点
和一个尾结点
,双向链表意味着我们
可以从头开始正向遍历,或者是从尾开始逆向遍历,并且可以针对头部和尾部进行相应的操作。
3、LinkedList的特性
在我们平常中,我们只知道一些常识性的特点:
1)是通过链表实现的
2)如果在频繁的插入,或者删除数据时,就用linkedList性能会更好。
那我们通过API去查看它的一些特性
1)Doubly-linked list implementation of the `List` and `Deque` interfaces.
Implements all optional list operations, and permits all elements (including
`null`).
这告诉我们,linkedList是一个双向链表,并且实现了List和Deque接口中所有的列表操作,并且能存
储任何元素,包括null,这里我们可以知道linkedList除了可以当链表使用,还可以当作队列使用,并
能进行相应的操作。
2)All of the operations perform as could be expected for a doubly-linked
list. Operations that index into the list will traverse the list from the
beginning or the end, whichever is closer to the specified index.
这个告诉我们,linkedList在执行任何操作的时候,都必须先遍历此列表来靠近通过index查找我们所
需要的的值。通俗点讲,这就告诉了我们这个是顺序存取,每次操作必须先按开始到结束的顺序遍历,随
机存取,就是arrayList,能够通过index。随便访问其中的任意位置的数据,这就是随机列表的意思。
3)api中接下来讲的一大堆,就是说明linkedList是一个非线程安全的(异步),其中在操作Interator时
,
如果改变列表结构
(add delete等),会发生fail-fast。
通过API再次总结一下LinkedList的特性:
1)异步,也就是非线程安全
2)双向链表。由于实现了list和Deque接口,能够当作队列来使用。
链表:查询效率不高,但是插入和删除这种操作性能好。
3)是顺序存取结构(注意和随机存取结构两个概念搞清楚)
4、LinkedList 源码分析
类的属性
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
// 实际元素个数
transient int size = 0;
// 头结点
transient Node<E> first;
// 尾结点
transient Node<E> last;
}
LinkedList的属性非常简单,一个头结点、一个尾结点、一个表示链表中实际元素个数的变量。注意,
头结点、尾结点都有transient关键字修饰,这也意味着在序列化时该域是不会序列化的。
构造方法
说明:会调用无参构造函数,并且会把集合中所有的元素添加到LinkedList中。
内部类(Node)
//根据前面介绍双向链表就知道这个代表什么了,linkedList的奥秘就在这里。
private static class Node<E> {
E item; // 数据域(当前节点的值)
Node<E> next; // 后继(指向当前一个节点的后一个节点)
Node<E> prev; // 前驱(指向当前节点的前一个节点)
// 构造函数,赋值前驱后继
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
说明:内部类Node就是实际的结点,用于存放实际元素的地方。
4、总结
- linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
- 能存储null值
- 跟arrayList相比较,就真正的知道了,LinkedList在删除和增加等操作上性能好,而ArrayList在查
询的性能上好 - 从源码中看,它不存在容量不足的情况
- linkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能
移除值。 - linkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。
Vector和Stack
前面写了一篇关于的是LinkedList的除了它的数据结构稍微有一点复杂之外,其他的都很好理解的。这
一篇讲的可能大家在开发中很少去用到。但是有的时候也可能是会用到的!
注意在学习这一篇之前,需要有多线程的知识:
- 锁机制:对象锁、方法锁、类锁
对象锁就是方法锁:就是在一个类中的方法上加上synchronized关键字,这就是给这个方法加锁了。
类锁:锁的是整个类,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象
被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。
其他线程继续被阻塞住。例如:在类A上有关键字synchronized,那么就是给类A加了类锁,线程1第一
个声明此类的实例,则线程1拿到了该类锁,线程2在想声明类A的对象,就会被阻塞。 - 在本文中,使用的是方法锁。
- 每个对象只有一把锁,有线程A,线程B,还有一个集合C类,线程A操作C拿到了集合中的锁(在
集合C中有用synchronized关键字修饰的),并且还没有执行完,那么线程A就不会释放锁,当轮到线程B
去操作集合C中的方法时 ,发现锁被人拿走了,所以线程B只能等待那个拿到锁的线程使用完,然后才能
拿到锁进行相应的操作。
Vector
1、Vector概述
通过API中可以知道:
- Vector是一个可变化长度的数组
- Vector增加长度通过的是capacity和capacityIncrement这两个变量,目前还不知道如何实现自动
扩增的,等会源码分析 - Vector也可以获得iterator和listIterator这两个迭代器,并且他们发生的是fail-fast,而不是failsafe,
注意这里,不要觉得这个vector是线程安全就搞错了,具体分析在下面会说 - Vector是一个
线程安全
的类,如果使用需要线程安全就使用Vector,如果不需要,就使用arrayList - Vector和ArrayList很类似,就少许的不一样,从它继承的类和实现的接口来看,跟arrayList一模一
样。
注意:现在的版本已经是jdk1.7,还有更高的jdk1.8了,在开发中,建议不用vector,原因在文章的
结束会有解释,如果需要线程安全的集合类直接用java.util.concurrent包下的类。
Stack
现在来看看Vector的子类Stack,学过数据结构都知道,这个就是栈的意思。那么该类就是跟栈的用法一
样了
class Stack<E> extends Vector<E> {}
通过查看他的方法,和查看api文档,很容易就能知道他的特性。就几个操作,出栈,入栈等,构造方法
也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也
是线程安全
。
总结
【Vector总结(通过源码分析)】
- Vector
线程安全
是因为它的方法都加了synchronized
关键字 - Vector的本质是一个
数组
,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关 - 它也会fail-fast,还有一个fail-safe两个的区别在下面的list总结中会讲到。
【Stack的总结】
- 对栈的一些操作,先进后出
- 底层也是用
数组
实现的,因为继承了Vector - 也是
线程安全的
List总结
【arrayList和LinkedList区别】
arrayList底层是用数组
实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长
度是0,只有在增加元素时,长度才会增加。默认是10
,不能无限扩增,有上限,在查询操作的时候性
能更好
LinkedList底层是用链表
来实现的,是一个双向链表
,注意这里不是双向循环链表,顺序存取类型。
在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性
能更好。
两个都是线程不安全
的,在iterator时,会发生fail-fast:快速失效
。
【arrayList和Vector的区别】
arrayList线程不安全,在用iterator,会发生fail-fast
Vector线程安全
,因为在方法前加了Synchronized
关键字。也会发生fail-fast
【fail-fast和fail-safe区别和什么情况下会发生】
简单的来说:在java.util下的集合都是发生fail-fast,而在java.util.concurrent下的发生的都是failsafe
。
1)fail-fast
快速失败,例如在arrayList中使用迭代器遍历时,有另外的线程对arrayList的存储数组进行了改变,比
如add、delete、等使之发生了结构上的改变,所以Iterator就会快速报一个
java.util.ConcurrentModificationException 异常(并发修改异常),这就是快速失败。
2)fail-safe
安全失败,在java.util.concurrent下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行
结构的改变,不会报异常,而是正常遍历,这就是安全失败。
3)为什么在java.util.concurrent包下对集合有结构的改变,却不会报异常?
在concurrent下的集合类增加元素的时候使用Arrays.copyOf()来拷贝副本,在副本上增加元素
,如果有
其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍
历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出
现一个副本。所以能防止fail-fast,这种机制并不会出错,所以我们叫这种现象为fail-safe。
4)vector也是线程安全的,为什么是fail-fast呢?
这里搞清楚一个问题,并不是说线程安全的集合就不会报fail-fast,而是报fail-safe,你得搞清楚前面所
说答案的原理,出现fail-safe是因为他们在实现增删的底层机制不一样,就像上面说的,会有一个副
本,而像arrayList、linekdList、verctor等,他们底层就是对着真正的引用进行操作,所以才会发生异
常。
5)既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢(也就是对其
删除和增加等操作)?
首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿vector来说,我们都没
有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办
法,因为那把锁就放在那边。
【举例说明fail-fast和fail-safe的区别】
- fail-fast
- fail-safe
通过CopyOnWriteArrayList这个类来做实验,不用管这个类的作用,但是他确实没有报异常,
并且还通过第二次打印,来验证了上面我们说创建了副本的事情。
原理是在添加操作时会创建副本
,在副本上进行添加操作,等迭代器遍历结束后,会将原引用
改为副本引用,所以我们在创建了一个list的迭代器,结果打印的就是123444了,
证明了确实改变成为了副本引用,后面为什么是三个4,原因是我们循环了3次,不久添加了3
个4吗。如果还感觉不爽的话,看下add的源码。
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Iterator iterator = list.iterator();
while (iterator.hasNext()){
list.add(5);
System.out.print(iterator.next() + " ");
}
System.out.println();
Iterator iterator1 = list.iterator();
while (iterator1.hasNext()){
System.out.print(iterator1.next() + " ");
}
//1 2 3 4
//1 2 3 4 5 5 5 5
【为什么现在都不提倡使用vector了】
1)vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中,
一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安
全。
2)如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,vector
本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销。
3)就如上面第三个问题所说的,vector还有fail-fast的问题
,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用arrayList,然后再加锁呢。
总结:Vector在你不需要进行线程安全的时候,也会给你加锁,也就导致了额外开销,所以在
jdk1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的
类。
Map
HashMap
1. HashMap数据结构
1.1 HashMap概述
HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。
在API中给出了相应的定义:
//1、哈希表基于map接口的实现,这个实现提供了map所有的操作,并且提供了key和value,可以为null,(HashMap和HashTable大致上是一样的,除了hashmap是异步的,和允许key和value为null),
//这个类不确定map中元素的位置,特别要提的是,这个类也不确定元素的位置随着时间会不会保持不变。
Hash table based implementation of the Map interface. This implementation
provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
//假设哈希函数将元素合适的分到了每个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操作(get、put)提供了稳定的性能,迭代这个集合视图需要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中)成正比,因此,如果迭代的性能很重要的话,就不要将初始容量设置的太高或者loadfactor设置的太低,【这里的桶,相当于在数组中每个位置上放一个桶装元素】
This implementation provides constant-time performance for the basic
operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the
"capacity" of the HashMap instance (the number of buckets) plus its size(the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (orthe load factor too low) if iteration performance is important.
//HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被创建时容量,
在容量自动增长之前加载因子是衡量哈希表被允许达到的多少的。当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被重新建立)所以哈希表有大约两倍的桶的数量.
An instance of HashMap has two parameters that affect its performance:
initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity,the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
//通常来讲,默认的加载因子(0.75)能够在时间和空间上提供一个好的平衡,更高的值会减少空间上的开支但是会增加查询花费的时间(体现在HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放entry的数量和加载因子,以便最少次数的进行rehash操作,如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff
between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum
number of entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的
容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table
1.2 HashMap在JDK1.8以前数据结构和存储原理
【链表散列】
首先我们要知道什么是链表散列?通过数组和链表结合在一起使用,就叫做链表散列。这其实就是
hashmap存储的原理图。
【HashMap的数据结构和存储原理】
HashMap的数据结构就是用的链表散列。那HashMap底层是怎么样使用这个数据结构进行数据存取的呢?分成两个部分:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key
和一个value,存到map中就会先将key和value保存在这个Entry类创建的对象中。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //就是我们说的map的key
V value; //value值,这两个都不陌生
Entry<K,V> next;//指向下一个entry对象
int hash;//通过key算过来的你hashcode值。
}
第二步:构造好了entry对象,然后将该对象放入数组中,如何存放就是这hashMap的精华所在了。
大概的一个存放过程是:通过entry对象中的hash值来确定将该对象存放在数组中的哪个位置上,如果在这个位置上还有其他元素,则通过链表来存储这个元素。
【Hash存放元素的过程】
通过key、value封装成一个entry对象,然后通过key的值
来计算该entry的hash值
,通过entry的hash值
和数组的长度length
来计算出entry放在数组中的哪个位置上面,每次存放都是将entry放在第一个位置。在这个过程中,就是通过hash值来确定将该对象存放在数组中的哪个位置上。
1.3 JDK1.8后HashMap的数据结构
上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
1.4 HashMap的属性
HashMap的实例有两个参数影响其性能。
初始容量
:哈希表中桶的数量(桶就是数组的容量)
加载因子
:哈希表在其容量自动增加之前可以达到多满的一种尺度
当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash
操作,将哈希表扩充至两倍
的桶数。
Java中默认初始容量为16
,加载因子为0.75
。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
【loadFactor加载因子】
定义:loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为
0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。loadFactor加载因子是控制数组存放数据的疏密程度
,loadFactor越趋近于1,那么数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,那么数组中存放的数据也就越稀,也就是可能数组中每个位置上就放一个元素。那有人说,就把loadFactor变为1
最好吗,存的数据很多,但是这样会有一个问题,就是我们在通过key拿到我们的value时,是先通过key的hashcode值,找到对应数组中的位置,如果该位置中有很多元素,则需要通过equals来依次比较链表中的元素,拿到我们的value值,这样花费的性能就很高
,如果能让数组上的每个位置尽量只有一个元素最好,我们就能直接得到value值了,所以有人又会说,那把loadFactor变得很小不就好了,但是如果变得太小,在数组中的位置就会太稀
,也就是分散的太开,浪费很多空间
,这样也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情况下不需要更改它。
【桶】
根据前面画的HashMap存储的数据结构图,你这样想,数组中每一个位置上都放有一个桶,每个桶里
就是装一个链表,链表中可以有很多个元素(entry),这就是桶的意思。也就相当于把元素都放在桶中。
【capacity】
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数。默认值是
16。
一般第一次扩容时会扩容到64
,之后好像是2倍
。总之,容量都是2的幂。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
【size的含义】
size就是在该HashMap的实例中实际存储的元素的个数
【threshold的作用】
int threshold;
threshold = capacity * loadFactor,当Size>=threshold
==(元素个数 > 数组容量 * 0.75)==的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是衡量数组是否需要扩增的一个标准。
注意这里说的是考虑,因为实际上要扩增数组,除了这个size>=threshold条件外,还需要另外一个条
件。
什么时候会扩增数组的大小?在put一个元素时先size>=threshold并且还要在对应数组位置上有元素,
这才能扩增数组。
我们通过一张HashMap的数据结构图来分析:
2. HashMap的源码分析
2.1 HashMap的层次关系与继承结构
【HashMap继承结构】
上面就继承了一个abstractMap,也就是用来减轻实现Map接口的编写负担。
【实现接口】
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
Map<K,V>:在AbstractMap抽象类中已经实现过的接口,这里又实现,实际上是多余的。但每个集合
都有这样的错误,也没过大影响
Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响
被拷贝的对象。
Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态。
2.2HashMap类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(size*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
2.3 HashMap的构造方法
有四个构造方法,构造方法的作用就是记录一下16这个数给threshold(这个数值最终会当作第一次数
组的长度。)和初始化加载因子。注意,hashMap中table数组一开始就已经是个没有长度的数组了。
构造方法中,并没有初始化数组的大小,数组在一开始就已经被创建了,构造方法只做两件事情,一个
是初始化加载因子,另一个是用threshold记录下数组初始化的大小。注意是记录。
【HashMap()】
//看上面的注释就已经知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75
//初始化容量:也就是初始化数组的大小
//加载因子:数组上的存放数据疏密程度。
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
【HashMap(int)】
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
【HashMap(int,float)】
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0,否则报错
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能大于最大值,否则为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 填充因子不能小于或等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 初始化填充因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
【HashMap(Map<? extends K, ? extends V> m)】
public HashMap(Map<? extends K, ? extends V> m) {
// 初始化填充因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将m中的所有元素添加至HashMap中
putMapEntries(m, false);
}
3. 常用方法
【put(K key,V value)】
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
【putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)】
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插
入元素的。
4. 总结
【关于数组扩容】
从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就
会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,
此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还
有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处
理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍
历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各
个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于
元素在桶中均匀分布所带来的好处。
【总结】
- 要知道hashMap在JDK1.8以前是一个链表散列这样一个数据结构,而在JDK1.8以后是一个数组加
链表加红黑树的数据结构。 - 通过源码的学习,hashMap是一个能快速通过key获取到value值得一个集合,原因是内部使用的
是hash查找值得方法。
迭代器
所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象
Iterator对象称作为迭代器,用以方便的对容器内元素的遍历操作,Iterator接口定义了如下方法:
- boolean hashNext();//判断是否有元素没有被遍历
- Object next();//返回游标当前位置的元素并将游标移动到下一个位置
- void remove();//删除游标左边的元素,在执行完
next之后该操作只能执行一次
执行next后才能进行remove,不然会异常fail-fast
问题:何遍历Map集合呢?
方法1:通过迭代器Iterator实现遍历 KeySet
获取Iterator :Collection 接口的iterator()方法
Iterator的方法:
- boolean hasNext(): 判断是否存在另一个可访问的元素
- Object next(): 返回要访问的下一个元素
HashMap dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");
Set keys=dogMap.keySet(); //取出所有key的集合
Iterator it=keys.iterator(); //获取Iterator对象
while(it.hasNext()){
String key=(String)it.next(); //取出key
System.out.println(dogMap.get(key)); //根据key取出对应的值
}
方法2:通过迭代器Iterator实现遍历EntrySet
HashMap<String,String> dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");
//HashMap定义的时候要加泛型这边才能自动生成
Iterator<Map.Entry<String, String>> iterator = dogMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, String> next = iterator.next();
System.out.println(next.getValue());
}
方法3:foreach
HashMap<String,String> dogMap = new HashMap();
dogMap.put("dog1","xiaoming");
dogMap.put("dog2","lisi");
for (Map.Entry<String, String> stringStringEntry : dogMap.entrySet()) {
System.out.println(stringStringEntry.getValue());
}
泛型
E:Element,在集合中使用,因为集合中存放的是元素。
T:Type,Java类。
K:Key,键。
V:Value,值。
N:Number,数值类型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许
程序员在编译时检测到非法的类型。
泛型的本质是参数化类型
,也就是说所操作的数据类型被指定为一个参数。
如何解决以下强制类型转换时容易出现的异常问题?
List的get(int index)方法获取元素
Map的get(Object key)方法获取元素
Iterator的next()方法获取元素
分析:通过泛型 , JDK1.5使用泛型改写了集合框架中的所有接口和类
? 通配符: < ? >
Collections工具类
【前言】
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进
行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
这个类不需要创建对象,内部提供的都是静态方法。
2、排序操作
1)static void reverse(List<?> list):
反转列表中元素的顺序。
2)static void shuffle(List<?> list) :
对List集合元素进行随机排序。
3) static void sort(List<T> list)
根据元素的自然顺序 对指定列表按升序进行排序
4)static <T> void sort(List<T> list, Comparator<? super T> c) :
根据指定比较器产生的顺序对指定列表进行排序。
5)static void swap(List<?> list, int i, int j)
在指定List的指定位置i,j处交换元素。
6)static void rotate(List<?> list, int distance)
当distance为正数时,将List集合的后distance个元素“整体”移到前面;当distance为
负数时,将list集合的前distance个元素“整体”移到后边。该方法不会改变集合的长度。
3、查找、替换操作
1) static <T> int binarySearch(List<? extends Comparable<? super T>>
list, T key)
使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引。
注意:此前必须保证List集合中的元素已经处于有序状态。
2)static Object max(Collection coll)
根据元素的自然顺序,返回给定collection 的最大元素。
3)static Object max(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最大元素。
4)static Object min(Collection coll):
根据元素的自然顺序,返回给定collection 的最小元素。
5)static Object min(Collection coll,Comparator comp):
根据指定比较器产生的顺序,返回给定 collection 的最小元素。
6) static <T> void fill(List<? super T> list, T obj) :
使用指定元素替换指定列表中的所有元素。
7)static int frequency(Collection<?> c, Object o)
返回指定 collection 中等于指定对象的出现次数。
8)static int indexOfSubList(List<?> source, List<?> target) :
返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回
-1。
9)static int lastIndexOfSubList(List<?> source, List<?> target)
返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回
-1。
10)static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
使用一个新值替换List对象的所有旧值oldVal
4、同步控制
Collectons提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从
而解决多线程并发访问集合时的线程安全问题。
正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的。
Collections提供了多个静态方法可以把他们包装成线程同步的集合。
1)static <T> Collection<T> synchronizedCollection(Collection<T> c)
返回指定 collection 支持的同步(线程安全的)collection。
2)static <T> List<T> synchronizedList(List<T> list)
返回指定列表支持的同步(线程安全的)列表。
3)static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定映射支持的同步(线程安全的)映射。
4)static <T> Set<T> synchronizedSet(Set<T> s)
返回指定 set 支持的同步(线程安全的)set。
import java.util.*;
public class TestSynchronized
{
public static void main(String[] args)
{
//下面程序创建了四个同步的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}
IO
1. 输入输出流分类
Java.io 包中定义了多个流类型(类或抽象类)来实现输入/输出功能;可以从不同的角度对其进行分类:
- 按数据流的方向不同可以分为输入流和输出流
- 按照处理数据单位不同可以分为字节流和字符流
- 按照功能不同可以分为节点流和处理流
我们来理解两个概念:
字节流
:最原始的一个流,读出来的数据就是010101这种最底层的数据表示形式,只不过它是按照字节来读的,一个字节(Byte)是8位(bit)读的时候不是一个位一个位的来读,而是一个字节一个字节来读。
字符流
:字符流是一个字符一个字符地往外读取数据。一个字符是2个字节
J2SDK所提供的所有流类型位于包 Java.io内,都分别继承自以下四种抽象流类型
。
输入流:InputStream(字节流),Reader(字符流)
输出流:OutPutStream(字节流),Writer(字符流)
这四个类都是抽象类,可以把这四个类想象成四根不同的管道。一端接着你的程序,另一端接着数据源,你可以通过输出管道从数据源里面往外读数据,也可以通过输入管道往数据源里面输入数据,总之,通过这四根管道可以让数据流进来和流出去。
io包里面定义了所有的流,所以一说流指的就是io包里面的
什么叫输入流?什么叫输出流?
用一根管道一端插进文件里,一端插进程序里面,然后开始读数据,那么这是输入还是输出呢?
如果站在文件的角度上,这叫输出。
如果站在程序的角度上,这叫输入。
记住,以后说输入流和输出流都是站在程序的角度上来说。
2. 节点流和处理流
你要是对原始的流不满意,你可以在这根管道外面再套其它的管道,套在其它管道之上的流叫处理流。
为什么需要处理流呢?这就跟水流里面有杂质,你要过滤它,你可以再套一层管道过滤这些杂质一样。
2.1.节点流类型
节点流就是一根管道直接插到数据源上面,直接读数据源里面的数据,或者是直接往数据源里面写入数据。典型的节点流是文件流:文件的字节输入流(FileInputStream),文件的字节输出流(FileOutputStream),文件的字符输入流(FileReader),文件的字符输出流(FileWriter)。
2.2.处理流类型
处理流是包在别的流上面的流,相当于是包到别的管道上面的管道。
3. InputStream(输入流)
我们看到的具体的某一些管道,凡是以InputStream结尾的管道,都是以字节的形式向我们的程序输入数据。
继承自InputStream的流都是用于向程序中输入数据,且数据的单位为字节(8bit);下图中深色为节点流,浅色为处理流。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qxi2TwyG-1671380809227)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221216173409018.png)]
3.1.InputStream的基本方法
//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException
read()方法是一个字节一个字节地往外读,每读取一个字节,就处理一个字节。read(byte[] buffer)方法读取数据时,先把读取到的数据填满这个byte[]类型的数组buffer(buffer是内存里面的一块缓冲区),然后再处理数组里面的数据。这就跟我们取水一样,先用一个桶去接,等桶接满水后再处理桶里面的水。
如果是每读取一个字节就处理一个字节,这样子读取也太累了。
3.2 案例
public class TestFileInputStream {
public static void main(String args[]) {
int b = 0;// 使用变量b来装调用read()方法时返回的整数
FileInputStream in = null;
// 使用FileInputStream流来读取有中文的内容时,读出来的是乱码,因为使用InputStream流里面的read()方法读取内容时是一个字节一个字节地读取的,而一个汉字是占用两个字节的,所以读取出来的汉字无法正确显示。
// FileReader in = null;
// 使用FileReader流来读取内容时,中英文都可以正确显示,因为Reader流里面的read()方法是一个字符一个字符地读取的,这样每次读取出来的都是一个完整的汉字,这样就可以正确显示了。
try {
in = new FileInputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
// in = new FileReader("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
} catch (FileNotFoundException e) {
System.out.println("系统找不到指定文件!");
System.exit(-1);// 系统非正常退出
}
long num = 0;// 使用变量num来记录读取到的字符数
// 调用read()方法时会抛异常,所以需要捕获异常
try {
while ((b = in.read()) != -1) {
// 调用int read() throws Exception方法时,返回的是一个int类型的整数
// 循环结束的条件就是返回一个值-1,表示此时已经读取到文件的末尾了。
// System.out.print(b+"\t");//如果没有使用“(char)b”进行转换,那么直接打印出来的b就是数字,而不是英文和中文了
System.out.print((char) b);
// “char(b)”把使用数字表示的汉字和英文字母转换成字符输入
num++;
}
in.close();// 关闭输入流
System.out.println();
System.out.println("总共读取了" + num + "个字节的文件");
} catch (IOException e1) {
System.out.println("文件读取错误!");
}
}
}
4. OutputStream(输出流)
继承自OutputStream的流是用于程序中输出数据,且数据的单位为字节(8bit):下图中深色的为节点流,浅色为处理流。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b26yDO62-1671380809227)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221216174735192.png)]
4.1.OutputStream的基本方法
//向输出流中写入一个字节数据,该字节数据为参数b的低8位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException
4.2 案例
public class TestFileOutputStream {
public static void main(String args[]) {
int b = 0;
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\Student.java");
out = new FileOutputStream("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
// 指明要写入数据的文件,如果指定的路径中不存在StudentNew.java这样的文件,则系统会自动创建一个
while ((b = in.read()) != -1) {
out.write(b);
// 调用write(int c)方法把读取到的字符全部写入到指定文件中去
}
in.close();
out.close();
} catch (FileNotFoundException e) {
System.out.println("文件读取失败");
System.exit(-1);// 非正常退出
} catch (IOException e1) {
System.out.println("文件复制失败!");
System.exit(-1);
}
System.out
.println("Student.StudentNew.java里面");
}
}
5. Reader流
Reader : 和InputStream一模一样,唯一的区别就在于读的数据单位不同
继承自Reader的流都是用于向程序中输入数据,且数据的单位为字符(16bit)
16位:一个字符也就是两个字节,使用Reader流读取数据时都是两个字节两个字节往外读的,为什么还要有这两种两个字节的读取方式呢? 因为有些字符是占2个字节的,如我们的中文字符在Java里面就是占两个字节的。如果采用一个字节一个字节往外读的方式,那么读出来的就是半个汉字,这样子Java就没有办法正确的显示中文字符的,所以有必要存在这种流,一个字符一个字符地往外读。
5.1.Reader的基本方法
//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException
6. Writer流
继承自Writer的流都是用于程序中输出数据,且数据的单位为字符(16bit);
6.1.Writer的基本方法
//向输出流中写入一个字节数据,该字节数据为参数b的低16位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException
6.2 演示
/*使用FileWriter(字符流)向指定文件中写入数据写入数据时以1个字符为单位进行写入*/
import java.io.*;
public class TestFileWriter{
public static void main(String args[]){
/*使用FileWriter输出流从程序把数据写入到Uicode.dat文件中
使用FileWriter流向文件写入数据时是一个字符一个字符写入的*/
FileWriter fw = null;
try{
fw = new FileWriter("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
//字符的本质是一个无符号的16位整数
//字符在计算机内部占用2个字节
//这里使用for循环把0~60000里面的所有整数都输出
//这里相当于是把全世界各个国家的文字都0~60000内的整数的形式来表示
for(int c=0;c<=60000;c++){
fw.write(c);
//使用write(int c)把0~60000内的整数写入到指定文件内
//调用write()方法时,我认为在执行的过程中应该使用了“(char)c”进行强制转换,即把整数转换成字符来显示
//因为打开写入数据的文件可以看到,里面显示的数据并不是0~60000内的整数,而是不同国家的文字的表示方式
}
/*使用FileReader(字符流)读取指定文件里面的内容
读取内容时是以一个字符为单位进行读取的*/
int b = 0;
long num = 0;
FileReader fr = null;
fr = new FileReader("E:\\教学\\班级\\Test\\Lesson2\\src\\com\\kuang\\chapter\\StudentNew.java");
while((b = fr.read())!= -1){
System.out.print((char)b + "\t");
num++;
}
System.out.println();
System.out.println("总共读取了"+num+"个字符");
}catch(Exception e) {
e.printStackTrace();
}
}
}
FileReader和FileWriter这两个流都是字符流,都是以一个字符为单位进行输入和输出的。所以读取和写入占用2个字节的字符时都可以正常地显示出来,以上是以File(文件)这个类型为例对节点流进行了讲解,所谓的节点流指定就是直接把输入流或输出插入到数据源上,直接往数据源里面写入数据或读取数据。
7. 处理流讲解
7.1.第一种处理流——缓冲流(Buffering)
缓冲流要”套接“在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加了一些新的方法。J2SDK提供了四种缓冲流,常用构造方法如下:
BufferedReader(Reader in)
BufferedReader(Reader in,int sz) //sz 为自定义缓冲区的大小
BufferedWriter(Writer out)
BufferedWriter(Writer out,int sz)
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in,int size)
BufferedOutputStream(InputStream in)
BufferedOutputStream(InputStream in,int size)
- 缓冲输入流支持其父类的mark和reset方法。
- BufferedReader提供了readLine方法用于读取一行字符串
- BufferedWriter提供了newLine用于写入一个行分隔符
- 对于输出的缓冲流,写出的数据会现在内存中缓存,使用
flush
方法将会使内存中的数据立刻写出
带有缓冲区的,缓冲区(Buffer)就是内存里面的一小块区域,读写数据时都是先把数据放到这块缓冲区
域里面,减少io对硬盘的访问次数,保护我们的硬盘。可以把缓冲区想象成一个小桶,把要读写的数据想象成水,每次读取数据或者是写入数据之前,都是先把数据装到这个桶里面,装满了以后再做处理。
这就是所谓的缓冲。先把数据放置到缓冲区上,等到缓冲区满了以后,再一次把缓冲区里面的数据写入到硬盘上或者读取出来,这样可以有效地减少对硬盘的访问次数,有利于保护我们的硬盘。
【缓冲流测试代码:BufferedInputStream】
public class Java01_Variable {
public static void main(String[] args) {
try {
FileInputStream in = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
FileInputStream out = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt.copy");
BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
System.out.println((char)bufferedInputStream.read());
int c = 0;
bufferedInputStream.mark(10);// 在第10个字符处做一个标记
for (int i = 0; i < 10 && (c = bufferedInputStream.read()) != -1; i++) {
System.out.print((char) c);
}
bufferedInputStream.reset();// 重新回到原来标记的地方
System.out.println();
for (int i = 0; i < 10 && (c = bufferedInputStream.read()) != -1; i++) {
System.out.print((char) c);
}
bufferedInputStream.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
【演示:BufferedReader BufferedWriter】
public class Java01_Variable {
public static void main(String[] args) {
try {
FileWriter out = new FileWriter("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
//在节点流FileWriter的外面再套一层处理流BufferedWriter
BufferedWriter bw = new BufferedWriter(out);
String s = null;
for(int i=0;i<100;i++){
//“Math.random()”将会生成一系列介于0~1之间的随机数。
// static String valueOf(double d)这个valueOf()方法的作用就是把一个double类型的数转换成字符串
//valueOf()是一个静态方法,所以可以使用“类型.静态方法名”的形式来调用
s = String.valueOf(Math.random());
bw.write(s);//把随机数字符串写入到指定文件中
bw.newLine();//调用newLine()方法使得每写入一个随机数就换行显示
}
bw.flush();//调用flush()方法清空缓冲区
//在节点流FileReader的外面再套一层处理流BufferedReader
BufferedReader br = new BufferedReader(new FileReader("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt"));
while((s = br.readLine())!=null){
//使用BufferedReader处理流里面提供String readLine()方法读取文件中的数据时是一行一行读取的
//循环结束的条件就是使用readLine()方法读取数据返回的字符串为空值后则表示已经读取到文件的末尾了。
System.out.println(s);
}
bw.close();
br.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
程序的输入指的是把从文件读取到的内容存储到为程序分配的内存区域里面去。流,什么是流,流无非就是两根管道,一根向里,一根向外,向里向外都是对于我们自己写的程序来说,流分为各种各样的类型,不同的分类方式又可以分为不同的类型,根据方向来分,分为输入流和输出流,根据读取数据的单位的不同,又可以分为字符流和字节流,除此之外,还可以分为节点流和处理流,节点流就是直接和数据源连接的流,处理流就是包在其它流上面的流,处理流不是直接和数据源连接,而是从数据源读取到数据以后再通过处理流处理一遍。缓冲流也包含了四个类:BufferedInputStream、BufferedOutputStream、BufferedReader和BufferedWriter。流都是成对的,没有流是是不成对的,肯定是一个in,一个out。
7.2.第二种处理流——转换流
InputStreamReader 和 OutputStreamWriter 用于字节数据到字符数据之间的转换
InputStreamReader 需要和 InputStream “套接” 。
OutputStreamWriter 需要和 OutputStream “套接” 。
转换流在构造时可以指定其编码集合
//把字节流转为字符流
InputStream isr = new InputStreamReader(System.in,"ISO8859-1")
转换流非常的有用,它可以把一个字节流转换成一个字符流
,转换流有两种,一种叫InputStreamReader,另一种叫OutputStreamWriter。InputStream是字节流,Reader是字符流,InputStreamReader就是把InputStream转换成Reader。OutputStream是字节流,Writer是字符流,OutputStreamWriter就是把OutputStream转换成Writer。把OutputStream转换成Writer之后就可以一个字符一个字符地通过管道写入数据了,而且还可以写入字符串。我们如果用一个FileOutputStream流往文件里面写东西,得要一个字节一个字节地写进去,但是如果我们在FileOutputStream流上面套上一个字符转换流,那我们就可以一个字符串一个字符串地写进去。
【转换流测试代码】
public class Java01_Variable {
public static void main(String[] args) {
try {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt"));
outputStreamWriter.write("QIUYUSY");
// 使用getEncoding()方法取得当前系统的默认字符编码
outputStreamWriter.getEncoding();
outputStreamWriter.close();
//如果在调用FileOutputStream的构造方法时没有加入true,那么新加入的字符
//串就会替换掉原来写入的字符串,在调用构造方法时指定了字符的编码
outputStreamWriter = new OutputStreamWriter(
new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt",true),"ISO8859_1");
outputStreamWriter.write("MircosoftsunIBMOracleApplet");// 再次向指定的文件写入字符串,新写入的字符串加入到原来字符串的后面
System.out.println(outputStreamWriter.getEncoding());
outputStreamWriter.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
try {
//System.in这里的in是一个标准的输入流,用来接收从键盘输入的数据
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(inputStreamReader);
String s = null;
s = br.readLine();//使用readLine()方法把读取到的一行字符串保存到字符串变量s中去
while (s != null) {
System.out.println(s.toUpperCase());//把保存在内存s中的字符串打印出来
s = br.readLine();//在循环体内继续接收从键盘的输入
if (s.equalsIgnoreCase("exit")) {
//只要输入exit循环就结束,就会退出
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
7.3 第三种处理流——数据流
DataInputStream 和 DataOutputStream 分别继承自InputStream 和 OutputStream , 它属于处
理流,需要分别“套接”在InputStream 和 OutputStream类型的节点流上。
DataInputStream 和 DataOutputStream 提供了可以存取与机器无关的Java原始类型数据(int,
double等)的方法。
DataInputStream 和 DataOutputStream 的构造方法
DataInputStream (InputStream in)
DataOutputStream (OutputStream out)
【数据流测试代码】
public static void main(String[] args) {
//在调用构造方法时,首先会在内存里面创建一个ByteArray字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//在输出流的外面套上一层数据流,用来处理int,double类型的数
DataOutputStream dos = new DataOutputStream(baos);
try{
dos.writeDouble(Math.random());//把产生的随机数直接写入到字节数组ByteArray中
dos.writeBoolean(true);//布尔类型的数据在内存中就只占一个字节
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
System.out.println(bais.available());
DataInputStream dis = new DataInputStream(bais);
System.out.println(dis.readDouble());//先写进去的就先读出来,调用readDouble()方法读取出写入的随机数
System.out.println(dis.readBoolean());//后写进去的就后读出来,这里面的读取顺序不能更改位置,否则会打印出不正确的结果
dos.close();
bais.close();
}catch(Exception e){
e.printStackTrace();
}
}
7.4.打印流——Print
PrintWriter 和 PrintStream 都属于输出流,分别针对与字符和字节
PrintWriter 和 PrintStream 提供了重载的print
Println方法用于多种数据类型的输出
PrintWriter和PrintStream的输出操作不会抛出异常,用户通过检测错误状态获取错误信息
PrintWriter 和 PrintStream有自动flush功能
PrintWriter(Writer out)
PrintWriter(Writer out,boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out,boolean autoFlush)
PrintStream(OutputStream out)
PrintStream(OutputStream out,boolean autoFlush)
/*这个小程序是重新设置打印输出的窗口,
* 把默认在命令行窗口输出打印内容设置成其他指定的打印显示窗口
*/
public static void main(String[] args) {
PrintStream printStream = null;
try {
FileOutputStream fileOutputStream = new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
//在输出流的外面套接一层打印流,用来控制打印输出
printStream = new PrintStream(fileOutputStream);
if(printStream != null){
//这里调用setOut()方法改变了输出窗口,以前写System.out.print()默认的输出窗口就是命令行窗口.
//但现在使用System.setOut(ps)将打印输出窗口改成了由ps指定的文件里面,通过这样设置以后,打印输出时都会在指定的文件内打印输出
//在这里将打印输出窗口设置到了log.txt这个文件里面,所以打印出来的内容会在log.txt这个文件里面看到
System.setOut(printStream);
}
for (char c = 0; c < 1000; c++) {
System.out.print(c+"\t");
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
7.5. 对象流——Object
直接将Object 写入或读出
transient
关键字- transient:透明的,用它来修饰的成员变量在序列化的时候不予考虑,也就是当成不存在。
- serializable接口
- externaliazble接口
public class Java01_Variable {
public static void main(String[] args) {
Student student = new Student();
student.k = 8;// 把k的值修改为8
try {
FileOutputStream fileOutputStream = new FileOutputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
// ObjectOutputStream流专门用来处理Object的
// 在fos流的外面套接ObjectOutputStream流就可以直接把一个Object写进去
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
oos.writeObject(student);// 直接把一个t对象写入到指定的文件里面
oos.flush();
oos.close();
FileInputStream fileInputStream = new FileInputStream("D:\\MyCode\\IDEA\\java-top-speed\\data\\word.txt");
// ObjectInputStream专门用来读一个Object的
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
// 直接把文件里面的内容全部读取出来然后分解成一个Object对象,并使用强制转换成指定类型T
Student sRead = (Student)ois.readObject();
System.out.println(sRead);
//Student{i=10, j=9, d=2.3, k=0} 可以看到k没有被序列化
ois.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
/*
* 凡是要将一个类的对象序列化成一个字节流就必须实现Serializable接口
* Serializable接口中没有定义方法,Serializable接口是一个标记性接口,用来给类作标记,只是起到一个标记作用。
* 这个标记是给编译器看的,编译器看到这个标记之后就可以知道这个类可以被序列化 如果想把某个类的对象序列化,就必须得实现Serializable接口
*/
class Student implements Serializable {
// Serializable的意思是可以被序列化的
int i = 10;
int j = 9;
double d = 2.3;
transient int k = 15;
// 在声明变量时如果加上transient关键字,那么这个变量就会被当作是透明的,即不存在。
@Override
public String toString() {
return "Student{" +
"i=" + i +
", j=" + j +
", d=" + d +
", k=" + k +
'}';
}
}
直接实现Serializable接口的类是JDK自动把这个类的对象序列化,而如果实现public interface
Externalizable extends Serializable的类则可以自己控制对象的序列化,建议能让JDK自己控制序列化
的就不要让自己去控制
8.IO流总结
多线程
线程简介
本章核心概念
- 线程就是独立的执行路径;
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
- main() 称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程创建(重点)
1. 继承Thread类(重点)
例子:多线程图片下载器
下载图片需要先导入
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author QiuYuSY
*/
//练习Thread,实现多线程同步下载图片
public class TestThread extends Thread {
private String url;
private String name;
public TestThread(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("文件名为"+name+"的文件下载完毕!");
}
public static void main(String[] args) {
TestThread t1 = new TestThread("https://www.kuangstudy.com/assert/course/c1/14.jpg","test1.jpg");
TestThread t2 = new TestThread("https://www.kuangstudy.com/assert/course/c1/13.jpg","test2.jpg");
TestThread t3 = new TestThread("https://www.kuangstudy.com/assert/course/c1/11.jpg","test3.jpg");
t1.start();
t2.start();
t3.start();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常,downloader出现问题");
}
}
}
2. 实现Runnable接口(重点)
public class TestThread implements Runnable {
private String url;
private String name;
public TestThread(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("文件名为"+name+"的文件下载完毕!");
}
public static void main(String[] args) {
//需要在Thread中new出继承Runable的对象
Thread t1 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/14.jpg", "test1.jpg"));
Thread t2 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/13.jpg", "test2.jpg"));
Thread t3 = new Thread(new TestThread("https://www.kuangstudy.com/assert/course/c1/11.jpg", "test3.jpg"));
t1.start();
t2.start();
t3.start();
}
}
多线程抢票问题
package org.example;
/**
* @author QiuYuSY
* @create 2022-12-16 23:21
*/
//多线程操作一个对象
//以买火车票为例子
public class TestThread4 implements Runnable {
private int ticketNums = 10;
@Override
public void run() {
while (true){
if(ticketNums <= 0){
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "拿到了第"+ticketNums--+"张票");
}
}
public static void main(String[] args) {
TestThread4 ticket= new TestThread4();
new Thread(ticket,"A").start();
new Thread(ticket,"B").start();
new Thread(ticket,"C").start();
}
}
//结果如下,这种时候就发生资源抢占的问题
B拿到了第8张票
C拿到了第10张票
A拿到了第9张票
B拿到了第7张票
C拿到了第7张票
A拿到了第6张票
C拿到了第5张票
A拿到了第5张票
B拿到了第5张票
C拿到了第4张票
B拿到了第3张票
A拿到了第3张票
B拿到了第2张票
C拿到了第1张票
A拿到了第2张票
案例:龟兔赛跑
public class Race implements Runnable{
//胜利者
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
//模拟兔子中途睡觉
if (Thread.currentThread().getName().equals("兔子") && i == 50){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//判断比赛是否结束
boolean flag = gameOver(i);
//比赛结束,结束程序
if (flag){
break;
}
System.out.println(Thread.currentThread().getName() + "跑了" + i +"步");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//判断比赛是否结束
private boolean gameOver(int steps){
//判断是否有胜利者
if(winner!=null){
return true;
}else{
//判断是否有人跑完了
if(steps >= 100){
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}else{
return false;
}
}
}
public static void main(String[] args) {
new Thread(new Race(),"兔子").start();
new Thread(new Race(),"乌龟").start();
}
}
3. 实现Callable接口(了解)
实现Callable接口需要重写call方法,call需要返回值(装箱)
实现Runnable接口重写run方法,无返回值(void)
/**
* callable的好处
* 1.可以定义返回值
* 2.可以抛出异常
*/
public class TestCallable implements Callable {
private String url;
private String name;
public TestCallable(String url, String name) {
this.url = url;
this.name = name;
}
@Override
public Boolean call() {
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.out.println("文件名为"+name+"的文件下载完毕!");
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/14.jpg", "test1.jpg");
TestCallable t2 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/12.jpg", "test2.jpg");
TestCallable t3 = new TestCallable("https://www.kuangstudy.com/assert/course/c1/11.jpg", "test3.jpg");
//创建执行服务:
ExecutorService server = Executors.newFixedThreadPool(2);
//提交执行
Future<Boolean> result1 = server.submit(t1);
Future<Boolean> result2 = server.submit(t2);
Future<Boolean> result3 = server.submit(t3);
//获取结果
boolean rs1 = result1.get();
boolean rs2 = result2.get();
boolean rs3 = result3.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//关闭服务
server.shutdownNow();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常,downloader出现问题");
}
}
}
静态代理模式
/*
静态代理模式总结:
1. 真实对象和代理对象都要实现同一个接口
2. 代理对象要代理真实对象
好处:
1. 代理对象可以做很多真实对象做不了的事情
2. 真实对象可以专注做自己的事情
*/
public class StaticProxy {
public static void main(String[] args) {
//对比一下线程Thread和静态代理
//线程创建了一个实现Runnable接口的对象,类似本例中的Marry接口,然后调用start,类似本例中的HappyMarry
new Thread( ()-> System.out.println("我爱你") ).start();
new WeddingCompany( new You() ).HappyMarry();
// WeddingCompany weddingCompany = new WeddingCompany(new You());
// weddingCompany.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
//真实角色
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("你要结婚了!");
}
}
//代理角色,婚庆公司,帮助你结婚
class WeddingCompany implements Marry{
//代理谁--》真实目标角色
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
//代理对象在调用真实对象之前之后都可以干一些别的事情
before();
target.HappyMarry(); //这就是真实对象,可以看到You类的HappyMarry是被婚庆公司调用的
after();
}
private void before() {
System.out.println("结婚前,布置现场");
}
private void after() {
System.out.println("结婚后,收尾款");
}
}
Lambda表达式
/**
* 逐渐简化的过程
*/
public class TestLambda {
//3. 使用静态内部类
static class B2 implements A{
@Override
public void test() {
System.out.println("静态内部类");
}
}
public static void main(String[] args) {
A a = new B();//多态
a.test();
a = new B2();
a.test();
//4. 局部内部类
class B3 implements A{
@Override
public void test() {
System.out.println("局部内部类");
}
}
a = new B3();
a.test();
//5. 匿名内部类
a = new A(){
@Override
public void test() {
System.out.println("匿名内部类");
}
};
a.test();
//6. lambda
a = ()->{
System.out.println("lambda");
};
a.test();
}
}
//1.定义一个函数式接口
interface A {
void test();
}
//2.使用实现类
class B implements A{
@Override
public void test() {
System.out.println("外部类实现");
}
}
再简化
public class TestLambda2 {
public static void main(String[] args) {
//lambda表示简化
interface_A inf = (int a)->{
System.out.println(a);
};
//简化1:参数类型
inf = (a)->{
System.out.println(a);
};
//简化2:简化括号 单个参数时可以简化括号
inf = a -> {
System.out.println(a);
};
//简化3: 去掉花括号,一行时才能使用
inf = a -> System.out.println(a);
//总结:
//lambda表达式只有在只含一行代码时才能简化成一行,如果有多行,就用代码块包裹
//前提接口为函数式接口
//多个参数时,也可以去掉参数类型,要去掉就都去掉,不能去掉括号
inf.A(2);
}
}
interface interface_A{
void A(int a);
}
线程状态
线程方法
线程停止
建议使用外部标志flag让线程自己停下来
线程睡眠 sleep
Sleep可以放大问题的不安全性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IMo7razV-1671380809231)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217021159536.png)]
//倒计时例子
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis());
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
线程礼让 yield
Join
观测线程状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cds1Vw4V-1671380809232)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217021347295.png)]
public class TestState {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("finish!");
});
System.out.println(thread.getState()); //NEW
thread.start();
System.out.println(thread.getState()); //RUNNABLE
//只要线程不中止,一直打印状态
while (thread.getState() != Thread.State.TERMINATED) {
Thread.sleep(100);
System.out.println(thread.getState()); //TIMED_WAITING
}
//TERMINATED
//线程死亡后无法再start
// thread.start();
}
}
线程优先级
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s5vSvbaQ-1671380809233)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217022847306.png)]
守护线程
setDaemon(true)
gc垃圾回收线程是守护线程
守护线程在程序停止的时候才停止
线程同步(重点)
多个线程操作同一个资源
解决的方法就是让线程排队
每个Object对象都拥有一把锁
锁造成的问题 保证了安全损失了性能
三大不安全案例
买票
package org.example.syn;
/**
* @author QiuYuSY
* @create 2022-12-17 2:44
*/
//不安全的买票
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread t1 = new Thread(buyTicket, "A");
Thread t2 = new Thread(buyTicket, "B");
Thread t3 = new Thread(buyTicket, "C");
t1.start();
t2.start();
t3.start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;
//外部循环停止标志
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
try {
buy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private void buy() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
}
//模拟延时
Thread.sleep(100);
//buy
System.out.println(Thread.currentThread().getName() + "拿到第" +ticketNums-- +"张票");
}
}
银行取钱
package org.example.syn;
/**
* @author QiuYuSY
* @create 2022-12-17 2:54
*/
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100,"账户");
Drawing A = new Drawing(account,50,"A");
Drawing B = new Drawing(account,100,"B");
A.start();
B.start();
}
}
class Account{
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account;
int drawingMoney;//取多少钱
int nowMoney;//手里的钱
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱
@Override
public void run() {
//判断有没有钱
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//卡内余额 = 余额 - 取的钱
account.money -= drawingMoney;
//手里的前
nowMoney += drawingMoney;
System.out.println(Thread.currentThread().getName() + "余额为" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);
}
}
B余额为-50
A余额为-50
A手里的钱50
B手里的钱100
不安全的集合
被覆盖了
public class UnsafeList {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
System.out.println(list.size()); //9992
}
}
同步方法
弊端
只读的不需要锁,修改的才需要上锁
同步块
- 同步方法锁对象本身(this)
- 同步块可以锁任何对象
不安全案例解决
买票
//安全的买票
public class SafeBuyTicket {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread t1 = new Thread(buyTicket, "A");
Thread t2 = new Thread(buyTicket, "B");
Thread t3 = new Thread(buyTicket, "C");
t1.start();
t2.start();
t3.start();
}
}
class BuyTicket implements Runnable{
private int ticketNums = 10;
//外部循环停止标志
boolean flag = true;
@Override
public void run() {
//买票
while (flag){
try {
//模拟延时
Thread.sleep(100);
buy();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//同步方法,锁的是this
private synchronized void buy() throws InterruptedException {
if (ticketNums <= 0) {
flag = false;
return;
}
//buy
System.out.println(Thread.currentThread().getName() + "拿到第" +ticketNums-- +"张票");
}
}
银行取钱
需要用同步块
package org.example.syn;
/**
* @author QiuYuSY
* @create 2022-12-17 2:54
*/
public class SafeBank {
public static void main(String[] args) {
Account account = new Account(100,"账户");
Drawing A = new Drawing(account,50,"A");
Drawing B = new Drawing(account,100,"B");
A.start();
B.start();
}
}
class Account{
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
//银行:模拟取款
class Drawing extends Thread{
Account account;
int drawingMoney;//取多少钱
int nowMoney;//手里的钱
public Drawing(Account account, int drawingMoney, String name) {
super(name);
this.account = account;
this.drawingMoney = drawingMoney;
}
//取钱 run方法加synchronized没用因为锁的是this,也就是银行,我们应该锁账户
@Override
public void run() {
//使用同步块来锁
synchronized (account){
//判断有没有钱
if(account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//卡内余额 = 余额 - 取的钱
account.money -= drawingMoney;
//手里的前
nowMoney += drawingMoney;
System.out.println(Thread.currentThread().getName() + "余额为" + account.money);
System.out.println(Thread.currentThread().getName() + "手里的钱" + nowMoney);
}
}
}
集合
public class SafeList {
public static void main(String[] args) throws InterruptedException {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
synchronized (list){
list.add(Thread.currentThread().getName());
}
}).start();
}
Thread.sleep(1000);
System.out.println(list.size());
}
}
JUC集合
concurrent下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lqjq7O9F-1671380809234)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217034337340.png)]
死锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRjUBKKw-1671380809234)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217181935421.png)]
例子
//死锁:多个线程互相拥有对方需要的资源,形成僵持
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0,"A");
MakeUp girl2 = new MakeUp(1,"B");
girl1.start();
girl2.start();
}
}
//口红
class LipStick{
}
//镜子
class Mirror{
}
class MakeUp extends Thread {
//static保证资源只有一份
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//化妆
private void makeup() throws InterruptedException {
if (choice == 0) {
//获得口红
synchronized (lipStick) {
System.out.println(girlName + "获得口红的锁");
Thread.sleep(1000);
//一秒后想获得镜子
synchronized (mirror) {
System.out.println(girlName +"获得镜子的锁");
}
}
} else {
//获得镜子
synchronized (mirror) {
System.out.println(girlName +"获得镜子的锁");
Thread.sleep(2000);
//一秒后想获得口红
synchronized (lipStick) {
System.out.println(girlName +"获得口红的锁");
}
}
}
}
}
解决
把第二个synchonized拿到外面
避免互同一个代码块拥有两个以上的锁,并且互相需要对方的锁
package org.example.deadlock;
/**
* @author QiuYuSY
* @create 2022-12-17 18:20
*/
//死锁:多个线程互相拥有对方需要的资源,形成僵持
public class DeadLock {
public static void main(String[] args) {
MakeUp girl1 = new MakeUp(0,"A");
MakeUp girl2 = new MakeUp(1,"B");
girl1.start();
girl2.start();
}
}
//口红
class LipStick{
}
//镜子
class Mirror{
}
class MakeUp extends Thread {
//static保证资源只有一份
static LipStick lipStick = new LipStick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
public MakeUp(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//化妆
private void makeup() throws InterruptedException {
if (choice == 0) {
//获得口红
synchronized (lipStick) {
System.out.println(girlName + "获得口红的锁");
Thread.sleep(1000);
}
//一秒后想获得镜子
synchronized (mirror) {
System.out.println(girlName +"获得镜子的锁");
}
} else {
//获得镜子
synchronized (mirror) {
System.out.println(girlName +"获得镜子的锁");
Thread.sleep(2000);
}
//一秒后想获得口红
synchronized (lipStick) {
System.out.println(girlName +"获得口红的锁");
}
}
}
}
Lock(锁)
- synchonized是隐式的锁 lock是显示的锁
ReentrantLock
可重入锁 实现了Lock接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JAI6BkKp-1671380809235)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217184023331.png)]
package org.example.syn;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author QiuYuSY
* @create 2022-12-17 18:43
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2,"A").start();
new Thread(testLock2,"B").start();
new Thread(testLock2,"C").start();
};
}
class TestLock2 implements Runnable{
private int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
Thread.sleep(1000);
//加锁
lock.lock();
if(ticketNums > 0){
System.out.println(Thread.currentThread().getName()+"买到"+ticketNums--);
}else{
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//解锁 unlock建议放在finally
lock.unlock();
}
}
}
}
synchronized和lock对比
线程通信
生产者消费者问题
synchronized只能实现线程同步,不能实现线程通信
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cVoi3dZE-1671380809236)(https://qiuyusy-img.oss-cn-hangzhou.aliyuncs.com/img/image-20221217190119879.png)]
所有的对象继承自Object,所有的对象都有自己的一把锁
解决方法1 管程法
package org.example.notice;
import com.sun.corba.se.impl.orbutil.concurrent.Sync;
/**
* @author QiuYuSY
* @create 2022-12-17 19:08
*/
//测试:生产者消费者模型--》利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
new Productor(synContainer).start();
new Consumer(synContainer).start();
}
}
//生产者
class Productor extends Thread{
SynContainer synContainer;
public Productor(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synContainer.push(new Chicken(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
//消费者
class Consumer extends Thread{
SynContainer synContainer;
public Consumer(SynContainer synContainer) {
this.synContainer = synContainer;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消费了"+synContainer.pop().id+"只鸡");
}
}
}
//产品
class Chicken{
int id; //产品编号
public Chicken(int id) {
this.id = id;
}
}
//缓冲区 读写数据的方法使用synchronized把容器锁住了
class SynContainer{
//需要一个容器大小
Chicken[] chickens = new Chicken[10];
//容器计数器
int count = 0;
//生产者放入产品
public synchronized void push(Chicken chicken){
//如果容器满了,等待消费者消费
if (count == chickens.length){
//通知消费者消费,生产者等待
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果容器没满,放入产品
chickens[count] = chicken;
count++;
//通知消费者消费
this.notifyAll();
}
//消费者消费产品
public synchronized Chicken pop(){
Chicken chicken = null;
//如果容器为空,等待生产者生产
if (count == 0){
//通知生产者生产,消费者等待
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//如果容器不为空,消费者消费
count--;
chicken = chickens[count];
//吃完了,通知生产者生产
this.notifyAll();
return chicken;
}
}
解决方法2 信号灯
用一个标志来判断
package org.example.notice;
/**
* @author QiuYuSY
* @create 2022-12-17 19:32
*/
//信号灯发,标志位解决
//生产者--演员 消费者--观众 产品--节目
//这里的观看和表演不是同步的,类似于电影,演员拍完后做成电影后观众才能看到
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//演员
class Player extends Thread{
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if(i % 2 == 0){
this.tv.play("快乐大本营");
}else{
this.tv.play("抖音");
}
}
}
}
//观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
//节目
class TV{
//演员表演,观众等待
//观众观看,演员等待
String voice; //表演的节目
boolean flag = true; //T时演员表演 F时演员等待
//表演
public synchronized void play(String voice){
//演员线程抢到cpu
// flag = false的时候演员等待
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("演员表演了:" + voice);
//通知观众来看
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
//观看
public synchronized void watch(){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("观众观看"+voice);
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
线程可以充分利用
- Runnable 使用 void execute 因为没有返回值
- Callable 使用submit 因为有返回值
package org.example.notice;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author QiuYuSY
* @create 2022-12-17 19:55
*/
public class TestPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.shutdownNow();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
总结
package org.example.notice;
import java.util.concurrent.*;
/**
* @author QiuYuSY
* @create 2022-12-17 20:02
*/
public class finishTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
new MyThread1().start();
new Thread(new MyThread2()).start();
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Integer> submit = service.submit(new MyThread3());
Integer integer = submit.get();
System.out.println("Callable返回值为"+integer);
service.shutdownNow();
//Callable的另一种实现方式
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());
new Thread(futureTask).start();
Integer integer1 = futureTask.get();
System.out.println("Callable返回值为"+integer1);
}
}
//1.继承Thread类
class MyThread1 extends Thread{
@Override
public void run() {
System.out.println("继承Thread类");
}
}
//2.实现Runnable接口
class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("实现Runnable接口");
}
}
//3.实现Callable接口
class MyThread3 implements Callable{
@Override
public Integer call() throws Exception {
System.out.println("实现Callable接口");
return 1;
}
}
网络编程
1. 网络编程概述
1.1、概述
Java是Internet上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序。Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由JVM进行控制。并且Java实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境。
计算机网络:
把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大,功能强的网络系统,从而使众多的计算机可以方便地互相传递信息,共享硬件,软件,数据信息等资源。
网络编程的目的:
直接或间接地通过网络协议与其他计算机实现数据交换,进行通讯。
网络编程中有两个主要的问题:
- 如何准确的定位网络上的一台或多台主机,定位主机上的特定的应用
192.168.12.124:端口 - 找到主机后如何可靠高效地进行数据传输
网络编程 != 网页编程(web开发)
B/S 架构 和 C/S 架构
1.2、网络通信两个要素
如何实现网络中的主机互相通信?
通信双方的地址 :
- IP 127.0.0.1
- 端口号 3306
【比如说我们这里上课使用的就是局域网,你们连接到我的电脑,就能查看到我电脑的画面了】
(即,网络通信协议,有两套参考模型)
- OSI 参考模型:模型过于理想化,未能在因特网上进行广泛推广!
- TCP/IP 参考模型:TCP/IP协议,事实上的国际标准。
小总结:
- 网络编程中有两个主要的问题:
- 如何准确的定位网络上一台或多台主机;定位主机上的特定的应用
- 找到主机后如何可靠高效的进行数据传输
- 网络编程中的两个要素:
- ip 和 端口号
- 提供网络通信协议。 TCP/IP参考模型(应用层,传输层,网络层,物理+数据链路层),
1.3、IP
ip地址:Inet Adderss
-
唯一的标识 internet 上的计算机 ( 通信实体 )
-
本地回环地址(hostAddress):
127.0.0.1
主机名 ( hostName ):localhost -
IP地址分类方式一1: IPV4 IPV6
- IPV4:4个字节组成,4个0~255。大概42亿个, 30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如
192.168.0.1
- IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用
冒号
隔开,如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
- IPV4:4个字节组成,4个0~255。大概42亿个, 30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如
-
IP地址分类方式2:公网地址(万维网使用) 和 私有地址(局域网使用)。
192.168.开头的就是私有地址
,范围即为 192.168.0.0 ~ 192.168.255.255,专门为组织机构内部使用
-
【查看 JDK 帮助文档=> InetAddress类,代表IP】
-
特点:不便于记忆,我们常使用域名来访问:www.baidu.com
-
https://blog.kuangstudy.com/ => DNS 域名解析(150.109.117.44)=> 现在本机的hosts文件,
判断是否有输入的域名地址,没有的话,再通过DNS服务器,找主机。
hosts文件地址: c:\windows\system32\drivers\etc\hosts
可以看到InetAddress类是没有构造方法的(黑的),所以无法new出来
InetAddress类
import java.net.InetAddress;
import java.net.UnknownHostException;
public class TestInetAddress {
public static void main(String[] args) {
try {
//根据域名获取IP
InetAddress address01 = InetAddress.getByName("localhost");
System.out.println(address01);
//获取本机地址
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
InetAddress address02 = InetAddress.getByName("www.baidu.com");
System.out.println(address02);
System.out.println();
System.out.println(address02.getCanonicalHostName());//180.101.49.13
System.out.println(address02.getHostAddress());//180.101.49.13
System.out.println(address02.getHostName());//www.baidu.com
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
}
1.4、端口号
端口号标识正在计算机上运行的进程(程序)
- 不同的进程有不同的端口号,用来区分软件
- 被规定为一个16位的整数 0~65535
- TCP 和 UDP 各有 65535个端口,单个协议下端口不能冲突
- 端口分类:
- 公认端口: 0~1023。被预先定义的服务通信占用端口。
- HTTP 默认端口 : 80
- HTTPS 默认端口:443
- FTP 默认端口: 21
- Telnet 默认端口:23
- 注册端口:1024~49151、分配给用户进程或应用程序。
- tomcat 默认端口:8080
- Mysql 默认端口:3306
- Oracle 默认端口:1521
- 动态、私有端口:49152~65535
- 公认端口: 0~1023。被预先定义的服务通信占用端口。
netstat -ano #查看所有端口
netstat -ano|findstr "6732" #查看指定端口
tasklist|findstr "6732" #查看指定进程
# 使用任务管理器查看PID
端口号与IP地址的组合,得出一个网络套接字:Socket
,所以说一些网络编程也被称为Socket编程
InetSocketAddress类
public class TestInetAddress {
public static void main(String[] args) {
InetSocketAddress socketAddress = new InetSocketAddress("www.baidu.com",8080);
System.out.println(socketAddress.getAddress());//www.baidu.com/180.101.49.14
System.out.println(socketAddress.getHostName());//www.baidu.com
System.out.println(socketAddress.getPort());//8080
}
}
1.5、网络通信协议
协议:就好比我们都说的普通话,大家才能听懂我讲的,但是我们还有自己的方言!
网络通信协议:
计算机网络中实现通信必须有一些约定,即通信协议,对速率,传输代码,代码结构,传输控制步骤,
出错控制等制定标准。
问题:网路协议太复杂?
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量
控制,路由控制,如何实现如此复杂的网络协议呢?
通信协议分层的思想
在制定协议时,把复杂成份分解成一些简单的成份,再将他们复合起来。最常用的复合方式是层次方
式,即同层间可以通信,上一层调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开
发和扩展。
TCP/IP协议簇
-
传输层协议中有两个非常重要的协议:
- 用户传输协议 TCP (Transmission Control Protocol)
传输层
- 用户数据报协议(User Datagram Protocol)
- 用户传输协议 TCP (Transmission Control Protocol)
-
TCP/IP 以其两个主要协议: 传输控制协议:TCP,和网络互联协议:IP,而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
-
IP
(Internet Protocol)协议是网络层
的主要协议,支持网间互联的数据通信。 -
TCP/IP协议模型从更实用的角度出发,形成了高效的
四层体系结构
,即物理链路层,IP层,传输层和应用层
TCP 和 UDP对比
- TCP协议
使用TCP协议前,必须建立TCP连接
,形成传输数据通道;- 传输前,采用
三次握手
方式,点对点通信,是可靠
的 - TCP协议进行通信的两个应用进程:客户端,服务端。
- 在连接中可进行大数据量的传输
- 传输完毕,
四次挥手
需要释放已建立的连接,效率低 - 举例:打电话
UDP协议
- 将数据,源,目的封装成数据包,
不需要建立连接
- 每个数据报的大小限制在64K内
- 发送方不管对方是否准备好,接收方收到也不确认,故事不可靠的
- 可以广播发送
- 发送数据结束时,无需释放资源,开销小,速度快。
- 举例:发短信,导弹(饱和攻击)
2、TCP网络编程
客户端
- 连接服务器socket
- 发送消息
服务器:
- 建立服务端口
- 等待客户端连接
- 读取客户的消息
2.1、案例一
需求:客户端发送信息给服务端,服务端将数据显示在控制台上。
TCPClientDemo01.java
//客户端
public class TCPClientDemo01 {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
try {
//1. 要知道服务器的地址,端口号
InetAddress serverIP = InetAddress.getByName("127.0.0.1");
int port = 9999;
//2.创建一个socket连接
socket = new Socket(serverIP,port);
//3.发送消息 IO流
os = socket.getOutputStream();
//String没法直接写入,需要转为byte数组
os.write("你好!".getBytes());
} catch (UnknownHostException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if(os != null){
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(socket!=null){
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
TCPServerDemo01.java
//服务端
public class TCPServerDemo01 {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1. 要有一个服务器地址
serverSocket = new ServerSocket(9999);
while(true){
//2. 等待客户端连接
socket = serverSocket.accept();
//3. 读取客户端的消息
is = socket.getInputStream();
/*回忆之前的IO流方案,弊端:存在中文,可能存在乱码。
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
String msg = new String(buffer,0,len);
System.out.println(msg);
}
*/
//管道流
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
//把buffer中的内容截出来放入 ByteArrayOutputStream
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if(baos != null){
try {
baos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (is != null){
try {
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (serverSocket != null){
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
2.2、案例二
需求:客户端发送文件给服务器,服务端将文件保存在本地。
我们需要准备一个图片,放在项目目录下:
TCPClientDemo02.java
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
public class TCPClientDemo02 {
public static void main(String[] args) {
try {
//1. 创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
//2. 创建一个输出流
OutputStream os = socket.getOutputStream();
//3. 读取本地文件,创建一个文件输入流
FileInputStream fis = new FileInputStream(new File("test1.png"));
//4. 写出文件
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
//通知服务器我已经发送完毕
socket.shutdownOutput();
//确定服务器接收完毕,才能断开连接
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while ((len2 = is.read(buffer2))!= -1){
baos.write(buffer2,0,len2);
}
System.out.println(baos.toString());
// 5. 关闭资源
baos.close();
is.close();
fis.close();
os.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
TCPServerDemo02.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServerDemo02 {
public static void main(String[] args) {
try {
//1. 创建服务
ServerSocket serverSocket = new ServerSocket(9999);
//2. 监听客户端连接 accept阻塞式监听,会一直等待客户端连接
Socket socket = serverSocket.accept();
//3. 获取输入流
InputStream is = socket.getInputStream();
//4. 文件输出
FileOutputStream fos = new FileOutputStream(new File("receive.jpg"));
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
//把buffer中的内容写入文件
fos.write(buffer,0,len);
}
//通知客户端接收完毕了
OutputStream os = socket.getOutputStream();
os.write("接收完毕".getBytes());
//关闭资源
fos.close();
is.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2.3、练习作业
- 服务端读取图片并发送给客户端,客户端保存图片到本地。
package org.example.net;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
public class TCPClientDemo04 {
public static void main(String[] args) {
try {
//1. 创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
//2. 创建一个输出流
OutputStream os = socket.getOutputStream();
//3. 输出信息
String picPath = "test1.png";
os.write(picPath.getBytes());
//通知服务器我已经发送完毕
socket.shutdownOutput();
//获取输入流
InputStream is = socket.getInputStream();
//处理流
FileOutputStream fos = new FileOutputStream(new File("receive.png"));
// 读取服务器发来的图片
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer))!= -1){
fos.write(buffer,0,len);
}
// 5. 关闭资源
fos.close();
is.close();
os.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package org.example.net;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServerDemo04 {
public static void main(String[] args) {
try {
//1. 创建服务
ServerSocket serverSocket = new ServerSocket(9999);
//2. 监听客户端连接 accept阻塞式监听,会一直等待客户端连接
Socket socket = serverSocket.accept();
//3. 获取输入流
InputStream is = socket.getInputStream();
//4. 处理流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 读取客户端发来的文本
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer))!= -1){
baos.write(buffer,0,len);
}
String picPath = baos.toString();
//获取输出流
OutputStream os = socket.getOutputStream();
//创建文件输入流,从服务器的本地获取文件
FileInputStream fis = new FileInputStream(new File(picPath));
byte[] buffer2 = new byte[1024];
int len2;
while ((len2 = fis.read(buffer2))!= -1){
os.write(buffer2,0,len2);
}
//关闭资源
fis.close();
os.close();
baos.close();
is.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
- 客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端。
package org.example.net;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
public class TCPClientDemo03 {
public static void main(String[] args) {
try {
//1. 创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
//2. 创建一个输出流
OutputStream os = socket.getOutputStream();
//3. 输出信息
os.write("hello world".getBytes());
//通知服务器我已经发送完毕
socket.shutdownOutput();
//获取输入流
InputStream is = socket.getInputStream();
//处理流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 读取服务器发来的文本
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer))!= -1){
baos.write(buffer,0,len);
}
String backString = baos.toString();
System.out.println(backString);
// 5. 关闭资源
baos.close();
is.close();
os.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package org.example.net;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServerDemo03 {
public static void main(String[] args) {
try {
//1. 创建服务
ServerSocket serverSocket = new ServerSocket(9999);
//2. 监听客户端连接 accept阻塞式监听,会一直等待客户端连接
Socket socket = serverSocket.accept();
//3. 获取输入流
InputStream is = socket.getInputStream();
//4. 处理流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 读取客户端发来的文本
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer))!= -1){
baos.write(buffer,0,len);
}
String readString = baos.toString();
//转大写
readString = readString.toUpperCase();
//获取输出流
OutputStream os = socket.getOutputStream();
//输出文本
os.write(readString.getBytes());
//关闭资源
os.close();
baos.close();
is.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2.4、 初识Tomcat服务器
客户端
- 用网络编程自己写 C
- 浏览器 B
服务端
- 用网络编程自己写
- Tomcat服务器
- 将提供的tomcat解压
- 运行bin目录下的启动文件
- 测试访问localhost:8080
-
现在,我们在里面的 \webapps\ROOT 目录下 新建一个kuangshen.txt,编辑文件保存!
-
浏览器输入 http://localhost:8080/kuangshen.txt ,发现可以访问到我们客户端的资源,OK
-
这里,我们的浏览器,就相当于是一个客户端,而Tomcat 服务器,就相当于是服务端,具体的关
于Tomcat的学习我们要到 JavaWeb 阶段会具体学习
3、UDP网络编程
3.1、说明
- DatagramSocket 和 DatagramPacket 两个类实现了基于UDP协议的网络程序。
- UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安
全送到目的地,也不确定什么时候可以抵达。 - DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端
的IP地址和端口号。 - UDP协议中每个数据报都给出了完整的地址信息,因此无需建立发送方和接收方的连接。如同发快
递包裹一样。
UDP没有服务器和客户端的说法,分为发送端和接收端
3.2、案例一
发送端
package org.example.net.udp;
import java.io.IOException;
import java.net.*;
// 不需要连接服务
public class UDPClientDemo01 {
public static void main(String[] args) throws IOException {
//1. 建立一个socket
DatagramSocket socket = new DatagramSocket();
//2. 建个包
String msg = "你好啊!服务器";
// 发送给谁
InetAddress localhost = InetAddress.getByName("localhost");
int port = 9999;
//数据,数据的长度起始,byte数组长度,发给谁,端口
DatagramPacket packet = new DatagramPacket(msg.getBytes(),
0,
msg.getBytes().length,
localhost,
port);
//3. 发送包 和TCP不同,发出去没连接也不会报错
socket.send(packet);
socket.close();
}
}
接收端
package org.example.net.udp;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//这个不算服务端,两边相互之间都能互相往端口发
public class UDPServerDemo01 {
public static void main(String[] args) throws IOException {
//开发端口
DatagramSocket socket = new DatagramSocket(9999);
//接受数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
socket.receive(packet); //阻塞接收
System.out.println(packet.getAddress().getHostName());
System.out.println(packet.getAddress().getHostAddress());
System.out.println(new String(packet.getData()));
//close
socket.close();
}
}
3.3、案例二:在线咨询
基础
//发送端
package org.example.net.chat;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
/**
* @author QiuYuSY
* @create 2022-12-18 18:55
*/
public class UdpSenderDemo01 {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
//控制台读取
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true){
String data = br.readLine();
// 发包
DatagramPacket packet = new DatagramPacket(
data.getBytes(),
0,
data.getBytes().length,
InetAddress.getByName("localhost"),
9999
);
socket.send(packet);
if(data.equals("bye")){
break;
}
}
}
}
//接收端
package org.example.net.chat;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* @author QiuYuSY
* @create 2022-12-18 18:56
*/
public class UdpReceiveDemo01 {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9999);
while (true){
//准备接受包
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
socket.receive(packet);
//输出数据
byte[] data = packet.getData();
String receiveData = new String(data, 0, data.length);
System.out.println(receiveData);
//断开连接 bye
if (receiveData.equals("bye")){
break;
}
}
}
}
多线程
写实现了Runnable的发送端和接收端
在构造函数中对socket这些进行初始化
package org.example.net.chat;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* @author QiuYuSY
* @create 2022-12-18 19:22
*/
public class TalkReceive implements Runnable{
DatagramSocket socket;
private int port;
private String msgFrom;
public TalkReceive(int port,String msgFrom) {
this.port = port;
this.msgFrom = msgFrom;
try {
socket = new DatagramSocket(port);
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
try {
while (true){
//准备接受包
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container, 0, container.length);
socket.receive(packet);
//输出数据
byte[] data = packet.getData();
String receiveData = new String(data, 0, data.length);
System.out.println(msgFrom + " 说: "+receiveData);
//断开连接 bye
if (receiveData.equals("bye")){
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
package org.example.net.chat;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
/**
* @author QiuYuSY
* @create 2022-12-18 19:22
*/
public class TalkSend implements Runnable{
DatagramSocket socket;
BufferedReader br;
private String toIP;
private int fromPort;
private int toPort;
public TalkSend(String toIP, int fromPort, int toPort) {
this.toIP = toIP;
this.fromPort = fromPort;
this.toPort = toPort;
try {
socket = new DatagramSocket(fromPort);
//控制台读取
br = new BufferedReader(new InputStreamReader(System.in));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true){
try {
String data = br.readLine();
// 发包
DatagramPacket packet = new DatagramPacket(
data.getBytes(),
0,
data.getBytes().length,
new InetSocketAddress(toIP,toPort)
);
socket.send(packet);
if(data.equals("bye")){
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
写一个学生类和一个老师类,每个类中都有一个发送线程和一个接受线程
public class TalkStudent {
public static void main(String[] args) {
new Thread(new TalkSend("localhost", 7777, 9999)).start();
new Thread(new TalkReceive(8888, "老师")).start();
}
}
public class TalkTeacher {
public static void main(String[] args) {
new Thread(new TalkSend("localhost", 5555, 8888)).start();
new Thread(new TalkReceive(9999, "学生")).start();
}
}
4、URL编程
4.1、url类
-
URL (Uniform Resource Locator): 统一资源定位符,它表示 internet 上某一资源的地址。
-
它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate:定位这个资源。
-
通过URL 我们可以访问Internet上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析
给定的URL可以在网络上查找相应的文件或其他资源。 -
URL 的 基本结构由 5部分组成:
传输协议://主1 机名:端口号/文件名 #片段名?参数列表
- 例如:http://localhost:8080/helloworld/index.jsp#a?username=kuangshen&password=123
- 片段名,即锚链接,比如我们去一些小说网站,可以直接定位到某个章节位置
- 参数列表格式 : 参数名=参数值 & 参数名=参数值…
package org.example.net.url;
import java.net.MalformedURLException;
import java.net.URL;
public class UrlDemo01 {
public static void main(String[] args) {
try {
URL url = new URL("http://localhost:8080/helloworld/index.jsp? username=kuangshen&password=123");
System.out.println(url.getProtocol()); //获取URL的协议名 http
System.out.println(url.getHost()); //获取URL的主机名 localhost
System.out.println(url.getPort()); //获取URL的端口号 8080
System.out.println(url.getPath()); //获取URL的文件路径 /helloworld/index.jsp
//获取URL的文件名 /helloworld/index.jsp? username=kuangshen&password=123
System.out.println(url.getFile());
//获取URL的查询名 username=kuangshen&password=123
System.out.println(url.getQuery());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
4.2、下载tomcat下的文件
webapps下创建一个qiuyu项目
在里头创建一个文件
package org.example.net.url;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @author QiuYuSY
* @create 2022-12-18 20:02
*/
public class UrlDown {
public static void main(String[] args) throws IOException {
//1. 地址
URL url = new URL("https://p1.music.126.net/DLVi_1eymwAX8gDunfd2bg==/109951165524394991.png");
//2. 连接到这个地址 http
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
//3.获取输入流
InputStream inputStream = urlConnection.getInputStream();
//4.写入文件
FileOutputStream fos = new FileOutputStream("music.png");
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read()) != -1 ){
fos.write(buffer,0,len);
}
//close
fos.close();
inputStream.close();
urlConnection.disconnect(); //断开连接
}
}
注解Annotation
1、什么是注解
- Annotation 是从JDK5.0开始引入的新技术 .
- Annotation的作用
- 不是程序本身 , 可以对程序作出解释.(这一点和注释(comment)没什么区别)
可以被其他程序(比如:编译器等)读取.
- Annotation的格式
- 注解是以"@注释名"在代码中存在的
- 还可以添加一些参数值 , 例如:@SuppressWarnings(value=“unchecked”)
- Annotation在哪里使用?
- 可以附加在package , class , method , field 等上面 , 相当于给他们添加了额外的辅助信息
- 我们可以通过反射机制实现对这些元数据的访问
2、内置注解
java中自带的一些注解
-
@Override
定义在 java.lang.Override 中 , 此注释只适用于修辞方法 , 表示一个方法声明打算重写超类中的另一个方法声明. -
@Deprecated
定义在java.lang.Deprecated中 , 此注释可以用于修辞方法 , 属性 , 类 ,
表示不鼓励程序员使用
这样的元素 , 通常是因为它很危险或者存在更好的选择 . -
@SuppressWarnings 镇压警告
定义在java.lang.SuppressWarnings中,用来抑制编译时的警告信息.
与前两个注释有所不同,你需要添加一个参数才能正确使用,这些参数都是已经定义好了的,我们选择性的使用就好了 .- @SuppressWarnings(“all”)
- @SuppressWarnings(“unchecked”)
- @SuppressWarnings(value={“unchecked”,“deprecation”})
- 等等 …
不加@SuppressWarnings时
加后警告都没了
3、元注解 meta-annotation
用于注释别的注解的注解
- 元注解的作用就是
负责注解其他注解
, Java定义了4个标准的meta-annotation
类型,他们被用来提供对其他annotation类型作说明 . - 这些类型和它们所支持的类在java.lang.annotation包中可以找到 .( @Target , @Retention ,@Documented , @Inherited )
- @Target : 用于描述注解的
使用范围
(即:被描述的注解可以用在什么地方) - @Retention : 表示需要在什么级别保存该注释信息 , 用于描述注解的生命周期
- (SOURCE < CLASS <
RUNTIME
) 一般都用RUNTIME
- (SOURCE < CLASS <
- @Document:说明该注解将被包含在javadoc中
- @Inherited:说明子类可以
继承
父类中的该注解
- @Target : 用于描述注解的
@MyAnnotation
public class Test02 {
@MyAnnotation
public void test(){
}
}
//自定义注解
//4个元注解用来注解其他注解
@Target(value = {ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@interface MyAnnotation{
}
4、自定义注解
- 使用 @interface自定义注解时 , 自动继承了java.lang.annotation.Annotation接口
- 分析 :
- @ interface用来声明一个注解 , 格式 : public @ interface 注解名 { 定义内容 }
- 其中的每一个
方法实际上是声明了一个配置参数
. - 方法的名称就是参数的名称.
- 返回值类型就是参数的类型 ( 返回值只能是基本类型,Class , String , enum ).
- 可以通过default来声明参数的默认值
- 如果只有一个参数成员 ,
并且
参数名为value,此时value = 可以不写,直接写参数 - 注解元素必须要有值 , 我们定义注解元素时 , 经常使用空字符串,0作为默认值 .
public class Test03 {
//注解可以显示赋值,如果没有加default我们必须给注解赋值
@MyAnnotation2(age = 18)
public void test(){
}
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2 {
//注解的参数: 参数类型 + 参数名(); 不是方法!
String name() default "";
int age();
int id() default -1; //如果默认值为-1代表不存在
String[] schools() default {"AAA","BBB"};
}
5、反射读取注解
看反射篇的ORM
反射 Reflection
1、静态 VS 动态语言
-
动态语言
-
是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
-
主要动态语言:Object-C、C#、JavaScript、PHP、Python等。
//体现动态语言的代码 function test() { var x = "var a=3;var b=5;alert(a+b)"; eval(x); }
-
-
静态语言
- 与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。
- Java不是动态语言,但Java可以称之为“准动态语言”。即Java有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。Java的动态性让编程的时候更加灵活!
2、Java Reflection
- Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
Class c = Class.forName("java.lang.String")
- 加载完类之后,在
堆内存的方法区
中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构
。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射
Java反射机制提供的功能
- 在运行时
判断任意一个对象所属的类
- 在运行时
构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时
获取泛型信息
- 在运行时调用任意一个对象的成员变量和方法
- 在运行时
处理注解
- 生成
动态代理
AOP中大量使用
…
Java反射优点和缺点
- 优点:可以实现动态创建对象和编译,体现出很大的
灵活性
! - 缺点:
对性能有影响
。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且满足我们的要求。这类操作总是慢于直接执行相同的操作。
public class Test01 {
public static void main(String[] args) throws ClassNotFoundException {
//一个类在内存中只有一个class对象
//一个类被加载后,类的整个结构都会被封装在class对象中
Class c1 = Class.forName("org.example.reflection.User");
Class c2 = Class.forName("org.example.reflection.User");
System.out.println(c1.hashCode() == c2.hashCode()); //true
c1.getInterfaces();
}
}
//实体类-- 类中只有一些属性 pojo entity 表示
class User{
private String name;
private int id;
private int age;
public User() {
}
public User(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
3、反射相关的主要API
java.lang.Class : 代表一个类
java.lang.reflect.Method : 代表类的方法
java.lang.reflect.Field : 代表类的成员变量
java.lang.reflect.Constructor : 代表类的构造器
4、Class类
Class类的常用方法
获取Class类的实例
public class Test03 {
public static void main(String[] args) throws ClassNotFoundException {
Person person = new Student();
//1. 通过对象获得
Class<? extends Person> c1 = person.getClass();
//2. 通过forname获得
Class<?> c2 = Class.forName("org.example.reflection.Student");
//3. 通过类名获得
Class<Student> c3 = Student.class;
//4. 基本数据类型的包装类有TYPE属性
Class<Integer> c4 = Integer.TYPE;
//5. 获得父类的类型
Class<?> c5 = c1.getSuperclass();
}
}
class Person{
public String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
}
class Student extends Person{
public Student() {
this.name = "Student";
}
}
class Teacher extends Person{
public Teacher() {
this.name = "Teacher";
}
}
哪些类型可以有Class对象?
- class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类。
- interface:接口
- []:数组
- enum:枚举
- annotation:注解@interface
- primitive type:基本数据类型
- void
public class Test04 {
public static void main(String[] args) {
Class c1 = Object.class; //类
Class c2 = Comparable.class; //接口
Class c3 = String[].class; //数组
Class c4 = int[][].class; //多维数组
Class c5 = ElementType.class; //枚举
Class c6 = Override.class; //注解
Class c7 = Integer.class; //包装类
Class c8 = void.class; //void
Class c9 = Class.class; //Class
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
System.out.println(c4);
System.out.println(c5);
System.out.println(c6);
System.out.println(c7);
System.out.println(c8);
System.out.println(c9);
System.out.println(c10);
System.out.println(c11);
//只要元素类型与维度一样,就是同一个Class
System.out.println(c11==c10); //true
}
}
5、Java内存分析
类的加载过程
public class Test05 {
public static void main(String[] args) {
A a = new A();
System.out.println(a.m); //100
/*
1. 加载到内存,会产生一个类对应Class对象
2. 链接,链接解锁后 m = 0
3. 初始化
<clinit>(){
System.out.println("A类静态代码块初始化");
m = 300;
m = 100; //覆盖了,所以结果为100
}
*/
}
}
class A{
static {
System.out.println("A类静态代码块初始化");
m = 300;
}
public static int m = 100;
public A() {
System.out.println("A类构造器代码初始化");
}
}
什么时候会发生类初始化?
- 类的主动引用(一定会发生类的初始化)
- 当虚拟机启动,先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
- 类的被动引用(不会发生类的初始化)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静
- 态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
类加载器的作用
双亲委派机制
如果你写了一个String的包,JVM会从下往上找,如果系统本身有个包在Bootstap 加载器中,那么你写的包就会失效,保证了安全性
6、创建运行时类的对象
通过反射获取运行时类的完整结构
Field、Method、Constructor、Superclass、Interface、Annotation
- 实现的全部接口
- 所继承的父类
- 全部的构造器
- 全部的方法
- 全部的Field
- 注解
- 。。。
package org.example.reflection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @author QiuYuSY
* @create 2022-12-18 23:21
*/
public class Test08 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
Class<?> c1 = Class.forName("org.example.reflection.User");
//获得类名
System.out.println(c1.getName()); //org.example.reflection.User
System.out.println(c1.getSimpleName()); //User
//获得属性
Field[] fields = c1.getFields();
for (Field field : fields) {
System.out.println(field);
}
//获得所有属性 包括private
Field[] declaredFields = c1.getDeclaredFields();
for (Field field : declaredFields) {
System.out.println(field);
}
/*private java.lang.String org.example.reflection.User.name
private int org.example.reflection.User.id
private int org.example.reflection.User.age*/
//获得指定属性
//Field id = c1.getField("id");
//System.out.println(id);
//获得指定属性 包括private
Field id2 = c1.getDeclaredField("id");
System.out.println(id2);
//获得本类及其父类的public方法
Method[] methods = c1.getMethods();
for (Method method : methods) {
System.out.println(method);
}
/*
public java.lang.String org.example.reflection.User.getName()
public int org.example.reflection.User.getId()
public void org.example.reflection.User.setName(java.lang.String)
public int org.example.reflection.User.getAge()
public void org.example.reflection.User.setAge(int)
public void org.example.reflection.User.setId(int)
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
*/
//获得本类及其父类的所有方法
Method[] declaredMethods = c1.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod);
}
//获得指定方法
Method getName = c1.getMethod("getName", null);
System.out.println(getName);
Method setName = c1.getMethod("setName", String.class);
System.out.println(setName);
//获得指定构造器
Constructor<?> constructor = c1.getConstructor(null);
System.out.println(constructor);
Constructor<?>[] constructors = c1.getConstructors();
for (Constructor<?> constructor1 : constructors) {
System.out.println(constructor1);
}
}
}
小结
- 在实际的操作中,取得类的信息的操作代码,并不会经常开发。
- 一定要熟悉java.lang.reflect包的作用,反射机制。
- 如何取得属性、方法、构造器的名称,修饰符等。
7、有了Class对象,能做什么?
- 创建类的对象:调用Class对象的newInstance()方法
- 类必须有一个无参数的构造器。
- 类的构造器的访问权限需要足够
- 思考?难道没有无参的构造器就不能创建对象了吗?只要在操作的时候明确的调用类中的构造器,
并将参数传递进去之后,才可以实例化操作。 - 步骤如下:
- 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型
的构造器 - 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
- 通过Constructor实例化对象
- 通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型
package org.example.reflection;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test09 {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
Class<?> c1 = Class.forName("org.example.reflection.User");
//本质上是调用了类的无参构造器
User user = (User) c1.newInstance();
System.out.println(user);
//通过构造器创建对象
Constructor<?> declaredClasses = c1.getDeclaredConstructor(String.class, int.class, int.class);
User user2 = (User) declaredClasses.newInstance("qiuyu", 1, 18);
System.out.println(user2);
//通过反射调用普通方法
User user3 = (User) c1.newInstance();
Method setName = c1.getDeclaredMethod("setName", String.class);
//激活方法 (对象,对象的值)
setName.invoke(user3, "qiuyubaba");
System.out.println(user3);
//通过反射操作属性
User user4 = (User) c1.newInstance();
Field name = c1.getDeclaredField("name");
// 设置权限,否则修改private会报权限不够
name.setAccessible(true);
name.set(user4, "qiuyu666");
System.out.println(user4);
}
}
8、setAccessible
- Method和Field、Constructor对象都有setAccessible()方法。
- setAccessible作用是
启动和禁用访问安全检查的开关
。 - 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
- 提高反射的
效率
。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。 - 使得原本无法访问的私有成员也可以访问
- 参数值为false则指示反射的对象应该实施Java语言访问检查
9、反射操作泛型
- Java采用
泛型擦除
的机制来引入泛型 , Java中的泛型仅仅是给编译器javac使用的,确保数据的安全性和免去强制类型转换问题 , 但是 , 一旦编译完成 , 所有和泛型有关的类型全部擦除 - 为了通过反射操作这些类型 , Java新增了 ParameterizedType , GenericArrayType , TypeVariable
和 WildcardType 几种类型来代表不能被归一到Class类中的类型但是又和原始类型齐名的类型. - ParameterizedType : 表示一种参数化类型,比如Collection
- GenericArrayType : 表示一种元素类型是参数化类型或者类型变量的数组类型
- TypeVariable : 是各种类型变量的公共父接口
- WildcardType : 代表一种通配符类型表达式
package org.example.reflection;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
//通过反射获取泛型
public class Test11 {
public static void main(String[] args) throws NoSuchMethodException {
Class<Test11> c1 = Test11.class;
Method test01 = c1.getMethod("test01", Map.class, List.class);
//获取泛型的参数类型
Type[] genericParameterTypes = test01.getGenericParameterTypes();
for (Type genericParameterType : genericParameterTypes) {
System.out.println(genericParameterType);
//如果内部还有继续
if (genericParameterType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
}
}
System.out.println("=================");
//获取返回值类型的泛型
Method test02 = c1.getMethod("test02", null);
Type genericReturnType = test02.getGenericReturnType();
System.out.println(genericReturnType);
if (genericReturnType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
}
}
public void test01(Map<String,User> map, List<User> list){
System.out.println("test01");
}
public Map<String,User> test02(){
System.out.println("test02");
return null;
}
}
10 反射获取注解
- getAnnotations
- getAnnotation
练习:ORM
package org.example.reflection;
import java.lang.annotation.*;
import java.lang.reflect.Field;
public class Test12 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
Class<?> c1 = Class.forName("org.example.reflection.Student2");
//通过反射获得注解
Annotation[] annotations = c1.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
//获取注解的value值
TableQiuyu tableQiuyu = c1.getAnnotation(TableQiuyu.class);
System.out.println(tableQiuyu.value());
//获得属性的注解
Field name = c1.getDeclaredField("name");
FieldQiuyu annotation = name.getAnnotation(FieldQiuyu.class);
System.out.println(annotation);
System.out.println(annotation.columnName());
System.out.println(annotation.type());
System.out.println(annotation.length());
/*
@org.example.reflection.TableQiuyu(value=db_student)
db_student
@org.example.reflection.FieldQiuyu(columnName=db_name, type=varchar, length=3)
db_name
varchar
3
*/
}
}
@TableQiuyu("db_student")
class Student2{
@FieldQiuyu(columnName = "db_id",type = "int",length = 10)
private int id;
@FieldQiuyu(columnName = "db_age",type = "int",length = 10)
private int age;
@FieldQiuyu(columnName = "db_name",type = "varchar",length = 3)
private String name;
public Student2() {
}
public Student2(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student2{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
//类名的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TableQiuyu{
String value();
}
//属性的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FieldQiuyu{
String columnName();
String type();
int length();
}