集合
什么是算法和数据结构
- 算法:
(1)可以解决具体问题 :例如 1+2+3+4+。。。+99+100
解题流程=算法
(2)有设计解决的具体的流程
算法1: 1+2=3 3+3=6 6+4=10…..加到100 –》5050
算法2:(1+100)50=10150=5050–>高斯算法
(3)有评价这个算法的具体的指标 –》时间复杂度 空间复杂度(从数学角度考虑)
- 数据结构:就是在计算机的缓存,内存,硬盘 如何组织管理数据的。重点在结构上,是按照什么结构来组织管理我们的数据。
- 数据结构分为:
(1)逻辑结构 :–》思想上的结构–》卧室,厨房,卫生间 —》线性表(数组,链表),图,树,栈,队列
(2)物理结构 :–》真实结构–》钢筋混凝土+牛顿力学——》紧密结构(顺序结构),跳转结构(链式结构)
- 紧密结构(顺序结构),跳转结构(链式结构)
以线性表为例:
线性表的逻辑结构如图所示:
- 线性表特点:
线性表是n个类型相同数据元素的有限序列,通常记作a0,a1,,,ai-1,ai,ai+1,,,,,an-1)。
相同数据类型
在线性表的定义中,我们看到从a0到an-1的n个数据元素是具有相同属件的亓素。
比如说可以都是数字,例如(12,23,45,56,45);
也可以是宇符,例如(A,B,….Z)
当然也可以是具有更复杂结构的数据元素,例如学生、商品、装备等。
相同数据类型意味着在内存中存储时,每个元素会占用相同的内存空间,便于后续的查询定位。
序列(顺序性)
在线性表的相邻数据元素之间存在若序偶关系,
即ai-1是ai的直接前驱,则ai是ai-1的直接后续,
同时ai又是ai+1的直接前驱,ai+1是ai的直接后续。
唯一没有直接前驱的元素a0 一端称为表头,唯一没有后续的元素an-1一端称为表尾。
除了表头和表尾元素外,任何一个元素都有且仅有一个直接前驱和直接后继。
有限
线件表中数据元素的个数n定义为线性表的长度, n是个有限值。
当n=0时线性表为空表,
在非空的线性表中每个数据元索在线性表中都有唯一确定的序号,
例如a0的序号是0 ,ai的序号是i。
在一个具有n>0个数据元素的线性表中,数据元素序号的范围是[O, n-1]。
逻辑结构和物理结构的关系:
线性表逻辑结构,对应的真实结构如果是紧密结构—》典型就是 数组:
线性表逻辑结构,对应的真实结构如果是跳转结构—》典型就是 链表:
优点:删除元素,插入元素效率高
缺点:查询元素效率低
集合的引入
数组,集合都是对多个数据进行存储操作的,简称为容器。
PS:这里的存储指的是内存层面的存储,而不是持久化存储(.txt,.avi,.jpg,数据库)。数组:特点:
(1)数组一旦指定了长度,那么长度就被确定了,不可以更改。
int[] arr = new int[6];
(2)数组一旦声明了类型以后,数组中只能存放这个类型的数据。数组中只能存放同一种类型的数据。
int[] arr,String[] s,double[] d…..数组:缺点:
(1)数组一旦指定了长度,那么长度就被确定了,不可以更改。
(2)删除,增加元素 效率低。
(3)数组中实际元素的数量是没有办法获取的,没有提供对应的方法或者属性来获取
(4)数组存储:有序,可重复 ,对于无序的,不可重复的数组不能满足要求。正因为上面的缺点,引入了一个新的存储数据的结构—》集合
集合一章我们会学习很多集合,为什么要学习这么多集合呢?
因为不同集合底层数据结构不一样。集合不一样,特点也不一样
简要集合结构图
集合应用场合
前端后端数据库交互:
当需要将相同结构的个体整合到一起的时候,需要集合。
实际应用场合:
泛型
引入
什么是泛型(Generic):
泛型就相当于标签
形式:<>
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,
JDK1.5之 后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。
Collection, List , ArrayList 这个 就是类型参数,即泛型。 没有泛型的时候使用集合:
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个ArrayList集合,向这个集合中存入学生的成绩:
ArrayList al = new ArrayList();
al.add(98);
al.add(18);
al.add(39);
al.add(60);
al.add(83);
al.add("丽丽");
//对集合遍历查看:
for(Object obj:al){
System.out.println(obj);
}
}
}
如果不使用泛型的话,有缺点:
一般我们在使用的时候基本上往集合中存入的都是相同类型的数据–》便于管理,所以现在什么引用数据类型都可以存入集合,不方便!
JDK1.5以后开始使用泛型,集合中使用泛型:
泛型总结:
(1) JDK1.5以后
(2) 泛型实际就是 一个<>引起来的 参数类型,这个参数类型 具体在使用的时候才会确定具体的类型。
(3)使用了泛型以后,可以确定集合中存放数据的类型,在编译时期就可以检查出来。
(4)使用泛型你可能觉得麻烦,实际使用了泛型才会简单,后续的遍历等操作简单。
(5)泛型的类型:都是引用数据类型,不能是基本数据类型。
(6)ArrayList
ArrayList
自定义泛型结构
泛型类,泛型接口
- 泛型类的定义和实例化
public class GenericTest<E> {
int age;
String name;
E sex;
public void a(E n){
}
public void b(E[] m){
}
}
class Test{
//这是main方法,程序的入口
public static void main(String[] args) {
//GenericTest进行实例化:
//(1)实例化的时候不指定泛型:如果实例化的时候不明确的指定类的泛型,那么认为此泛型为Object类型
GenericTest gt1 = new GenericTest();
gt1.a("abc");
gt1.a(17);
gt1.a(9.8);
gt1.b(new String[]{"a","b","c"});
//(2)实例化的时候指定泛型:---》推荐方式
GenericTest<String> gt2 = new GenericTest<>();
gt2.sex = "男";
gt2.a("abc");
gt2.b(new String[]{"a","b","c"});
}
}
继承情况:
(1)父类指定泛型:
class SubGenericTest extends GenericTest<Integer>{
}
class Demo{
//这是main方法,程序的入口
public static void main(String[] args) {
//指定父类泛型,那么子类就不需要再指定泛型了,可以直接使用
SubGenericTest sgt = new SubGenericTest();
sgt.a(19);
}
}
(2)父类不指定泛型:
如果父类不指定泛型,那么子类也会变成一个泛型类,那这个E的类型可以在创建子类对象的时候确定:
class SubGenericTest2<E> extends GenericTest<E>{
}
class Demo2{
//这是main方法,程序的入口
public static void main(String[] args) {
SubGenericTest2<String> s = new SubGenericTest2<>();
s.a("abc");
s.sex = "女";
}
}
- 应用场合:
- 细节:
(1)泛型类可以定义多个参数类型
(2)泛型类的构造器的写法:
(3)不同的泛型的引用类型不可以相互赋值:
(4)泛型如果不指定,那么就会被擦除,反应对应的类型为Object类型:
(5)反省类中的静态方法不能使用类的泛型
(6)不能直接使用E[]的创建:
泛型方法
/**
* @author : msb-zhaoss
* 1.什么是泛型方法:
* 不是带泛型的方法就是泛型方法
* 泛型方法有要求:这个方法的泛型的参数类型要和当前的类的泛型无关
* 换个角度:
* 泛型方法对应的那个泛型参数类型 和 当前所在的这个类 是否是泛型类,泛型是啥 无关
* 2.泛型方法定义的时候,前面要加上<T>
* 原因:如果不加的话,会把T当做一种数据类型,然而代码中没有T类型那么就会报错
* 3.T的类型是在调用方法的时候确定的
* 4.泛型方法可否是静态方法?可以是静态方法
*/
public class TestGeneric<E> {
//不是泛型方法 (不能是静态方法)
public static void a(E e){
}
//是泛型方法
public static <T> void b(T t){
}
}
class Demo{
//这是main方法,程序的入口
public static void main(String[] args) {
TestGeneric<String> tg = new TestGeneric<>();
tg.a("abc");
tg.b("abc");
tg.b(19);
tg.b(true);
}
}
泛型参数存在继承关系的情况
通配符
- 在没有通配符的时候:
下面的a方法,相当于方法的重复定义,报错
public class Test {
/*public void a(List<Object> list){
}
public void a(List<String> list){
}
public void a(List<Integer> list){
}*/
}
- 引入通配符:
public class Demo {
//这是main方法,程序的入口
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<Integer> list3 = new ArrayList<>();
List<?> list = null;
list = list1;
list = list2;
list = list3;
}
}
发现: A 和 B是子类父类的关系,G和G不存在子类父类关系,是并列的
加入通配符?后,G<?>就变成了 G和G的父类
- 使用通配符:
public class Test {
/*public void a(List<Object> list){
}
public void a(List<String> list){
}
public void a(List<Integer> list){
}*/
public void a(List<?> list){
//内部遍历的时候用Object即可,不用?
for(Object a:list){
System.out.println(a);
}
}
}
class T{
//这是main方法,程序的入口
public static void main(String[] args) {
Test t = new Test();
t.a(new ArrayList<Integer>());
t.a(new ArrayList<String>());
t.a(new ArrayList<Object>());
}
}
- 查看API中应用位置:
使用通配符后的细节
public class Test {
public void a(List<?> list){
//1.遍历:
for(Object a:list){
System.out.println(a);
}
//2.数据的写入操作 :
//list.add("abc");-->出错,不能随意的添加数据
list.add(null);
//3.数据的读取操作:
Object s = list.get(0);
}
}
class T{
//这是main方法,程序的入口
public static void main(String[] args) {
Test t = new Test();
t.a(new ArrayList<Integer>());
t.a(new ArrayList<String>());
t.a(new ArrayList<Object>());
}
}
泛型受限
public class Test {
//这是main方法,程序的入口
public static void main(String[] args) {
//a,b,c三个集合是并列的关系:
List<Object> a = new ArrayList<>();
List<Person> b = new ArrayList<>();
List<Student> c = new ArrayList<>();
/*开始使用泛型受限:泛型的上限
List<? extends Person>:
就相当于:
List<? extends Person>是List<Person>的父类,是List<Person的子类>的父类
*/
List<? extends Person> list1 = null;
/*list1 = a;
list1 = b;
list1 = c;*/
/*开始使用泛型受限:泛型的下限
List<? super Person>
就相当于:
List<? super Person>是List<Person>的父类,是List<Person的父类>的父类
*/
List<? super Person> list2 = null;
list2 = a;
list2 = b;
list3 = c;
}
}
Collection接口
collection接口常用方法
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
/*
Collection接口的常用方法:
增加:add(E e) addAll(Collection<? extends E> c)
删除:clear() remove(Object o)
修改:
查看:iterator() size()
判断:contains(Object o) equals(Object o) isEmpty()
*/
//创建对象:接口不能创建对象,利用实现类创建对象:
Collection col = new ArrayList();
//调用方法:
//集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型
//基本数据类型自动装箱,对应包装类。int--->Integer
col.add(18);
col.add(12);
col.add(11);
col.add(17);
System.out.println(col/*.toString()*/);
List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});
col.addAll(list);//将另一个集合添加入col中
System.out.println(col);
//col.clear();清空集合
System.out.println(col);
System.out.println("集合中元素的数量为:"+col.size());
System.out.println("集合是否为空:"+col.isEmpty());
boolean isRemove = col.remove(15);
System.out.println(col);
System.out.println("集合中数据是否被删除:"+isRemove);
Collection col2 = new ArrayList();
col2.add(18);
col2.add(12);
col2.add(11);
col2.add(17);
Collection col3 = new ArrayList();
col3.add(18);
col3.add(12);
col3.add(11);
col3.add(17);
System.out.println(col2.equals(col3));
System.out.println(col2==col3);//地址一定不相等 false
System.out.println("是否包含元素:"+col3.contains(117));
}
}
Collection集合的遍历
迭代器简要原理图
public class Test02 { //这是main方法,程序的入口 public static void main(String[] args) { Collection col = new ArrayList(); col.add(18); col.add(12); col.add(11); col.add(17); col.add("abc"); col.add(9.8); //对集合遍历(对集合中元素进行查看) //方式1:普通for循环 /*for(int i= 0;i<col.size();i++){ col. }*/ //方式2:增强for循环 for(Object o:col){ System.out.println(o); } System.out.println("------------------------"); //方式3:iterator() Iterator it = col.iterator(); while(it.hasNext()){ System.out.println(it.next()); } } }
方法 描述 public boolean add(E o) 向集合中插入对象 public boolean addAll(Collection<?extends E>c) 将集合的内容插入进来 public void clear() 清除此集合中的所有元素 public bollean contains(Object o) 判断某一个对象是否在集合中存在 public bollean containsAll(Collection<?>c) 判断一组对象是否在集合中存在 public boolean equals(Object o) 对象比较 public int hashCode() 哈希码 public boolean isEmpty() 集合是否为空 public Iterator iterator() 为Iterator接口实例化 public boolean remove(Object o) 删除指定对象 public boolean removeAll(Collection<?>c) 删除一组对象 public boolean retainAll(Collection<?>c) 保存指定内容 public int size() 求出集合的大小 public Objectp[] toArray() 将一个集合变为对象数组 public T[]toArray(T[]a) 指定好返回的对象数组类型
List接口
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
/*
List接口中常用方法:
增加:add(int index, E element)
删除:remove(int index) remove(Object o)
修改:set(int index, E element)
查看:get(int index)
判断:
*/
List list = new ArrayList();
list.add(13);
list.add(17);
list.add(6);
list.add(-1);
list.add(2);
list.add("abc");
System.out.println(list);
list.add(3,66);
System.out.println(list);
list.set(3,77);
System.out.println(list);
list.remove(2);//在集合中存入的是Integer类型数据的时候,调用remove方法调用的是:remove(int index)
System.out.println(list);
list.remove("abc");
System.out.println(list);
Object o = list.get(0);
System.out.println(o);
//List集合 遍历:
//方式1:普通for循环:
System.out.println("---------------------");
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//方式2:增强for循环:
System.out.println("---------------------");
for(Object obj:list){
System.out.println(obj);
}
//方式3:迭代器:
System.out.println("---------------------");
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
}
ArrayList实现类(JDK1.7)
- 在idea中切换JDK的方法:
- ArrayList实现List接口的失误:
集合创始人 承认了这个失误,但是在后续的版本中没有删除,觉得没必要:
- 底层重要属性:
在JDK1.7中:在调用构造器的时候给底层数组elementData初始化,数组初始化长度为10:
对应内存:
调用add方法:
ArrayList al = new ArrayList();
System.out.println(al.add("abc"));
System.out.println(al.add("def"));
当数组中的10个位置都满了的时候就开始进行数组的扩容,扩容长度为 原数组的1.5倍:
ArrayList实现类(JDK1.8)
- JDK1.8底层依旧是Object类型的数组,size:数组中有效长度:
- ArrayList al = new ArrayList();调用空构造器:
- add方法:
- 底层Object数组,int类型属性表示数组中有效长度:
- Vector v=new Vector();调用构造器:
- add方法:
Vector实现类
NO | 比较点 | ArrayList(Java) | Vector(C++) |
---|---|---|---|
1 | 推出时间 | JDK1.2 | JDK1.0 |
2 | 性能 | 异步处理 性能高 | 同步处理 性能低 |
3 | 线程安全 | 非线程安全操作类 | 线程安全操作类 |
4 | 输出速度 | Iterator ForEach快 | Enumeration输出慢 |
- 底层Object数组,int类型属性表示数组中有效长度:
Vector v=new Vector();调用构造器:
- add方法:
LinkedList实现类的使用
LinkedList | 效果 |
---|---|
addFirst(E o) | 在开头增加数据 |
addLast(E o) | 在结尾增加数据 |
element() | 获取不移除此列表的头(第一个元素) |
peek() | 获取并不移除此列表的头(第一个元素) |
poll() | 获取并移除此列表的头(第一个元素) |
removeFirst() | 移除第一个元素 |
removeLast() | 移除最后一个元素 |
LinkedList简要底层原理图
模拟LinkedList源码
public class MyLinkedList {
//链中一定有一个首节点:
Node first;
//链中一定有一个尾节点:
Node last;
//计数器:
int count = 0;
//提供一个构造器:
public MyLinkedList(){
}
//添加元素方法:
public void add(Object o){
if(first == null){//证明你添加的元素是第一个节点:
//将添加的元素封装为一个Node对象:
Node n = new Node();
n.setPre(null);
n.setObj(o);
n.setNext(null);
//当前链中第一个节点变为n
first = n;
//当前链中最后一个节点变为n
last = n;
}else{//证明已经不是链中第一个节点了
//将添加的元素封装为一个Node对象:
Node n = new Node();
n.setPre(last);//n的上一个节点一定是当前链中的最后一个节点last
n.setObj(o);
n.setNext(null);
//当前链中的最后一个节点的下一个元素 要指向n
last.setNext(n);
//将最后一个节点变为n
last = n;
}
//链中元素数量加1
count++;
}
//得到集合中元素的数量:
public int getSize(){
return count;
}
//通过下标得到元素:
public Object get(int index){
//获取链表的头元素:
Node n = first;
//一路next得到想要的元素
for(int i=0;i<index;i++){
n = n.getNext();
}
return n.getObj();
}
}
class Test{
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个MyLinkedList集合对象:
MyLinkedList ml = new MyLinkedList();
ml.add("aa");
ml.add("bb");
ml.add("cc");
System.out.println(ml.getSize());
System.out.println(ml.get(0));
}
}
debug验证数据添加成功:
LinkedList源码解析
【1】JDK1.7和JDK1.8的LinkedList的源码是一致的
【2】源码:
public class LinkedList<E>{//E是一个泛型,具体的类型要在实例化的时候才会最终确定
transient int size = 0;//集合中元素的数量
//Node的内部类
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;
}
}
transient Node<E> first;//链表的首节点
transient Node<E> last;//链表的尾节点
//空构造器:
public LinkedList() {
}
//添加元素操作:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {//添加的元素e
final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
//将元素封装为一个Node具体的对象:
final Node<E> newNode = new Node<>(l, e, null);
//将链表的last节点指向新的创建的对象:
last = newNode;
if (l == null)//如果添加的是第一个节点
first = newNode;//将链表的first节点指向为新节点
else//如果添加的不是第一个节点
l.next = newNode;//将l的下一个指向为新的节点
size++;//集合中元素数量加1操作
modCount++;
}
//获取集合中元素数量
public int size() {
return size;
}
//通过索引得到元素:
public E get(int index) {
checkElementIndex(index);//健壮性考虑
return node(index).item;
}
Node<E> node(int index) {
//如果index在链表的前半段,那么从前往后找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//如果index在链表的后半段,那么从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
}
面试题: Iterator(),Iterator,Iterable关系
- 面试题:对应的关系:
- hasNext(),next()的具体实现:
- 增强for循环 底层也是通过迭代器实现的:
ListIterator迭代器
- 加入字符串
public class Test2 {
//这是main方法,程序的入口
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
//在"cc"之后添加一个字符串"kk"
Iterator<String> it = list.iterator();
while(it.hasNext()){
if("cc".equals(it.next())){
list.add("kk");
}
}
}
}
发现报错:
出错原因:就是迭代器和list同时对集合进行操作:
解决办法:事情让一个“人”做 –》引入新的迭代器:ListIterator
迭代和添加操作都是靠ListIterator来完成的:
public class Test2 {
//这是main方法,程序的入口
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
//在"cc"之后添加一个字符串"kk"
ListIterator<String> it = list.listIterator();
while(it.hasNext()){
if("cc".equals(it.next())){
it.add("kk");
}
}
System.out.println(it.hasNext());
System.out.println(it.hasPrevious());
//逆向遍历:
while(it.hasPrevious()){
System.out.println(it.previous());
}
System.out.println(it.hasNext());
System.out.println(it.hasPrevious());
System.out.println(list);
}
}
Set接口
HashSet实现类的使用
- 放入Integer类型数据
public class TestInteger {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个HashSet集合:
HashSet<Integer> hs = new HashSet<>();
System.out.println(hs.add(19));//true
hs.add(5);
hs.add(20);
System.out.println(hs.add(19));//false 这个19没有放入到集合中
hs.add(41);
hs.add(0);
System.out.println(hs.size());//唯一,无序
System.out.println(hs);
}
}
- 放入String类型数据:
public class TestString {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个HashSet集合:
HashSet<String> hs = new HashSet<>();
hs.add("hello");
hs.add("apple");
hs.add("banana");
hs.add("html");
hs.add("apple");
hs.add("css");
System.out.println(hs.size());
System.out.println(hs);
}
}
- 放入自定义的引用数据类型的数据:
public class TestStudent {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个HashSet集合:
HashSet<Student> hs = new HashSet<>();
hs.add(new Student(19,"lili"));
hs.add(new Student(20,"lulu"));
hs.add(new Student(18,"feifei"));
hs.add(new Student(19,"lili"));
hs.add(new Student(10,"nana"));
System.out.println(hs.size());
System.out.println(hs);
}
}
上面自定义的类型不满足 唯一,无序的特点。为什么呢?
- HashSet原理图:(简要原理图)
- 疑问:
1.数组的长度是多少。
2.数组的类型是什么?
3.hashCode,equals方法真的调用了吗?验证
4.底层表达式是什么?
5.同一个位置的数据 向前放 还是 向后放?
6.放入数组中的数据,是直接放的吗?是否封装为对象了?
LinkedHashSet使用
其实就是在HashSet的基础上,多了一个总的链表,这个总链表将放入的元素串在一起,方便有序的遍历:(可以看到LinkedHashMap.Entry 继承自HashMap.Node 除了Node 本身有的几个属性外,额外增加了before after 用于指向前一个Entry 后一个Entry。也就是说,元素之间维持着一条总的链表数据结构。)
public class TestInteger {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个HashSet集合:
LinkedHashSet<Integer> hs = new LinkedHashSet<>();
System.out.println(hs.add(19));//true
hs.add(5);
hs.add(20);
System.out.println(hs.add(19));//false 这个19没有放入到集合中
hs.add(41);
hs.add(0);
System.out.println(hs.size());//唯一,无序
System.out.println(hs);
}
}
比较器的使用
- 以int类型为案例:
比较的思路:将比较的数据做差,然后返回一个int类型的数据,将这个int类型的数值 按照 =0 >0 <0
int a = 10;
int b = 20;
System.out.println(a-b); // =0 >0 <0
- 比较String类型数据:
String类实现了Comparable接口,这个接口中有一个抽象方法compareTo,String类中重写这个方法即可
String a = "A";
String b = "B";
System.out.println(a.compareTo(b));
- 比较double类型数据:
double a = 9.6;
double b = 9.3;
/* System.out.println((int)(a-b));*/
System.out.println(((Double) a).compareTo((Double) b));
- 比较自定义的数据类型:
(1)内部比较器:
public class Student implements Comparable<Student>{
private int age;
private double height;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student(int age, double height, String name) {
this.age = age;
this.height = height;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", height=" + height +
", name='" + name + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
//按照年龄进行比较:
/*return this.getAge() - o.getAge();*/
//按照身高比较
/*return ((Double)(this.getHeight())).compareTo((Double)(o.getHeight()));*/
//按照名字比较:
return this.getName().compareTo(o.getName());
}
}
public class Test02 {
//这是main方法,程序的入口
public static void main(String[] args) {
//比较两个学生:
Student s1 = new Student(14,160.5,"alili");
Student s2 = new Student(14,170.5,"bnana");
System.out.println(s1.compareTo(s2));
}
}
(2)外部比较器:
public class Student{
private int age;
private double height;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student(int age, double height, String name) {
this.age = age;
this.height = height;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", height=" + height +
", name='" + name + '\'' +
'}';
}
}
class BiJiao01 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//比较年龄:
return o1.getAge()-o2.getAge();
}
}
class BiJiao02 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//比较姓名:
return o1.getName().compareTo(o2.getName());
}
}
class BiJiao03 implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//在年龄相同的情况下 比较身高 年龄不同比较年龄
if((o1.getAge()-o2.getAge())==0){
return ((Double)(o1.getHeight())).compareTo((Double)(o2.getHeight()));
}else{//年龄不一样
return o1.getAge()-o2.getAge();
}
}
}
public class Test02 {
//这是main方法,程序的入口
public static void main(String[] args) {
//比较两个学生:
Student s1 = new Student(9,160.5,"alili");
Student s2 = new Student(14,170.5,"bnana");
//获取外部比较器:
Comparator bj1 = new BiJiao03();
System.out.println(bj1.compare(s1, s2));
}
}
- 外部比较器和内部比较器 谁好呀?
答案:外部比较器,多态,扩展性好
TreeSet实现类的使用
- 存入Integer类型数据:(底层利用的是内部比较器)
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个TreeSet:
TreeSet<Integer> ts = new TreeSet<>();
ts.add(12);
ts.add(3);
ts.add(7);
ts.add(9);
ts.add(3);
ts.add(16);
System.out.println(ts.size());
System.out.println(ts);
}
}
特点:唯一,无序(没有按照输入顺序进行输出), 有序(按照升序进行遍历)
- 原理:底层:二叉树(数据结构中的一个逻辑结构)
- 放入String类型数据:(底层实现类内部比较器)
public class Test02 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个TreeSet:
TreeSet<String> ts = new TreeSet<>();
ts.add("elili");
ts.add("blili");
ts.add("alili");
ts.add("elili");
ts.add("clili");
ts.add("flili");
ts.add("glili");
System.out.println(ts.size());
System.out.println(ts);
}
}
- 想放入自定义的Student类型的数据:
(1)利用内部比较器:
public class Student implements Comparable<Student> {
private int age;
private String name;
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;
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public int compareTo(Student o) {
return this.getAge()-o.getAge();
}
}
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个TreeSet:
TreeSet<Student> ts = new TreeSet<>();
ts.add(new Student(10,"elili"));
ts.add(new Student(8,"blili"));
ts.add(new Student(4,"alili"));
ts.add(new Student(9,"elili"));
ts.add(new Student(10,"flili"));
ts.add(new Student(1,"dlili"));
System.out.println(ts.size());
System.out.println(ts);
}
}
(2)通过外部比较器:
public class Student {
private int age;
private String name;
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;
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
class BiJiao implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
}
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个TreeSet:
//利用外部比较器,必须自己制定:
Comparator<Student> com = new BiJiao();
TreeSet<Student> ts = new TreeSet<>(com);//一旦指定外部比较器,那么就会按照外部比较器来比较
ts.add(new Student(10,"elili"));
ts.add(new Student(8,"blili"));
ts.add(new Student(4,"alili"));
ts.add(new Student(9,"elili"));
ts.add(new Student(10,"flili"));
ts.add(new Student(1,"dlili"));
System.out.println(ts.size());
System.out.println(ts);
}
}
实际开发中利用外部比较器多,因为扩展性好(多态)
换一种写法:
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
//创建一个TreeSet:
//利用外部比较器,必须自己制定:
/*Comparator<Student> com = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
};*/
TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
});//一旦指定外部比较器,那么就会按照外部比较器来比较
ts.add(new Student(10,"elili"));
ts.add(new Student(8,"blili"));
ts.add(new Student(4,"alili"));
ts.add(new Student(9,"elili"));
ts.add(new Student(10,"flili"));
ts.add(new Student(1,"dlili"));
System.out.println(ts.size());
System.out.println(ts);
}
}
- TreeSet底层的二叉树的遍历是按照升序的结果出现的,这个升序是靠中序遍历得到的:
SortedSet
- first: 返回集合中第一个元素
- last: 返回集合中最后一个元素
- headSet(item i):返回从开始到指定的元素集合
- tailSet: 返回指定元素到最后
- subSet: 返回指定对象间的元素
Conllection部分整体结构图
Map接口
HashMap常用方法
NO | 方法或类 | 描述 |
---|---|---|
1 | public void clear() | 清空Map集合 |
2 | public bollean containsKey(Object Key) | 判断指定的key是否存在 |
3 | public boolean cotainsValue(Object value) | 判断指定的value是否存在 |
4 | public Set<Map Entry<K,V>> entrySet() | 将Map对象变为Set集合 |
5 | public boolean equals(Object o) | 对象比较 |
6 | public V get(Object key) | 根据key取得value |
7 | public int hashCode() | 返回哈希码 |
8 | public boolean isEmpty() | 判断集合是否为空 |
9 | public Set |
取得所有的key |
10 | public V put(K key,V value) | 向集合中加入元素 |
11 | public void putAll(Map<?extends K,?extends V>t) | 将一个Map集合中的内容加入到另一个Map |
12 | public V remove(Object key) | 根据key删除value |
13 | public int size() | 取出集合的长度 |
14 | public Collection |
取出全部的value |
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
/*
增加:put(K key, V value)
删除:clear() remove(Object key)
修改:
查看:entrySet() get(Object key) keySet() size() values()
判断:containsKey(Object key) containsValue(Object value)
equals(Object o) isEmpty()
*/
//创建一个Map集合:无序,唯一
Map<String,Integer> map = new HashMap<>();
System.out.println(map.put("lili", 10101010));
map.put("nana",12345234);
map.put("feifei",34563465);
System.out.println(map.put("lili", 34565677));
map.put("mingming",12323);
/*map.clear();清空*/
/*map.remove("feifei");移除*/
System.out.println(map.size());
System.out.println(map);
System.out.println(map.containsKey("lili"));
System.out.println(map.containsValue(12323));
Map<String,Integer> map2 = new HashMap<>();
System.out.println(map2.put("lili", 10101010));
map2.put("nana",12345234);
map2.put("feifei",34563465);
System.out.println(map2.put("lili", 34565677));
map2.put("mingming2",12323);
System.out.println(map==map2);
System.out.println(map.equals(map2));//equals进行了重写,比较的是集合中的值是否一致
System.out.println("判断是否为空:"+map.isEmpty());
System.out.println(map.get("nana"));
System.out.println("-----------------------------------");
//keySet()对集合中的key进行遍历查看:
Set<String> set = map.keySet();
for(String s:set){
System.out.println(s);
}
System.out.println("-----------------------------------");
//values()对集合中的value进行遍历查看:
Collection<Integer> values = map.values();
for(Integer i:values){
System.out.println(i);
}
System.out.println("-----------------------------------");
//get(Object key) keySet()
Set<String> set2 = map.keySet();
for(String s:set2){
System.out.println(map.get(s));
}
System.out.println("-----------------------------------");
//entrySet()
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> e:entries){
System.out.println(e.getKey()+"----"+e.getValue());
}
}
}
HashMap和HashTable的区别
- HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(Key),由于非线程安全,异步操作,效率上可能高于Hashtable.
- HashMap允许将null作为一个entry的key或者value,而Hashtable不允许
- HashMap把Hashtable的contains方法去掉了,改成containscalue和containsKey. 因为contains方法容易让人引起误解
TreeMap
- key的类型为String类型:
public class Test02 {
//这是main方法,程序的入口
public static void main(String[] args) {
Map<String,Integer> map = new TreeMap<>();
map.put("blili",1234);
map.put("alili",2345);
map.put("blili",5467);
map.put("clili",5678);
map.put("dlili",2345);
System.out.println(map.size());
System.out.println(map);
}
}
- key的类型是一个自定义的引用数据类型:
(1)内部比较器:
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
Map<Student,Integer> map = new TreeMap<>();
map.put(new Student(19,"blili",170.5),1001);
map.put(new Student(18,"blili",150.5),1003);
map.put(new Student(19,"alili",180.5),1023);
map.put(new Student(17,"clili",140.5),1671);
map.put(new Student(10,"dlili",160.5),1891);
System.out.println(map);
System.out.println(map.size());
}
}
public class Student implements Comparable<Student>{
private int age;
private String name;
private double height;
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;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public Student(int age, String name, double height) {
this.age = age;
this.name = name;
this.height = height;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", height=" + height +
'}';
}
@Override
public int compareTo(Student o) {
/* return this.getAge()-o.getAge();*/
return this.getName().compareTo(o.getName());
}
}
(2)外部比较器:
public class Test03 {
//这是main方法,程序的入口
public static void main(String[] args) {
Map<Student,Integer> map = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return ((Double)(o1.getHeight())).compareTo((Double)(o2.getHeight()));
}
});
map.put(new Student(19,"blili",170.5),1001);
map.put(new Student(18,"blili",150.5),1003);
map.put(new Student(19,"alili",180.5),1023);
map.put(new Student(17,"clili",140.5),1671);
map.put(new Student(10,"dlili",160.5),1891);
System.out.println(map);
System.out.println(map.size());
}
}
源码部分
代码展示特性
public class Test {
//这是main方法,程序的入口
public static void main(String[] args) {
//JDK1.7以后支持后面的<>中内容可以不写
HashMap<Integer,String> hm = new HashMap<>();
System.out.println(hm.put(12,"丽丽"));
System.out.println(hm.put(7,"菲菲"));
System.out.println(hm.put(19,"露露"));
System.out.println(hm.put(12,"明明"));
System.out.println(hm.put(6,"莹莹"));
System.out.println("集合的长度:"+hm.size());
System.out.println("集合中内容查看:"+hm);
}
}
展示结果
先演示原理图,再看源码,直接看的话,有的人接不上就蒙了:
相当于先看原理,然后从源码中验证这个原理是否正确:把图搞懂了,就是事倍功半的效果
原理如下:(JDK1.7)
源码(JDK1.7版本)
public class HashMap<K,V>
extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口
//【2】又实现了这个接口,多余,但是设计者觉得没有必要删除,就这么地了
implements Map<K,V>, Cloneable, Serializable{
//【3】后续会用到的重要属性:先粘贴过来:
static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
//定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
//太大容易引起哈西冲突,太小容易浪费 0.75是经过大量运算后得到的最好值
//这个值其实可以自己改,但是不建议改,因为这个0.75是大量运算得到的
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
transient int size;
int threshold;//数组扩容的界限值,门槛值 16*0.75=12
final float loadFactor;//用来接收装填因子的变量
//【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
public HashMap(int initialCapacity, float loadFactor) {
//【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
this.loadFactor = loadFactor;
//【8】数组扩容的界限值,门槛值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//【9】给table数组赋值,初始化数组长度为16
table = new Entry[capacity];
}
//【10】调用put方法:
public V put(K key, V value) {
//【11】对空值的判断
if (key == null)
return putForNullKey(value);
//【12】调用hash方法,获取哈希码
int hash = hash(key);
//【14】得到key对应在数组中的位置
int i = indexFor(hash, table.length);
//【16】如果你放入的元素,在主数组那个位置上没有值,e==null 那么下面这个循环不走
//当在同一个位置上放入元素的时候
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//哈希值一样 并且 equals相比一样
//(k = e.key) == key 如果是一个对象就不用比较equals了
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//【17】走addEntry添加这个节点的方法:
addEntry(hash, key, value, i);
return null;
}
//【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
//k.hashCode()函数调用的是key键值类型自带的哈希函数,
//由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
/*
接下来的一串与运算和异或运算,称之为“扰动函数”,
扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,
增加其值的不确定性,从而降低冲突的概率。
不同的版本实现的方式不一样,但其根本思想是一致的。
往右移动的目的,就是为了将h的高位利用起来,减少哈西冲突
*/
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//【15】返回int类型数组的坐标
static int indexFor(int h, int length) {
//其实这个算法就是取模运算:h%length,取模效率不如位运算
return h & (length-1);
}
//【18】调用addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//【25】size的大小 大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
if ((size >= threshold) && (null != table[bucketIndex])) {
//【26】主数组扩容为2倍
resize(2 * table.length);
//【30】重新调整当前元素的hash码
hash = (null != key) ? hash(key) : 0;
//【31】重新计算元素位置
bucketIndex = indexFor(hash, table.length);
}
//【19】将hash,key,value,bucketIndex位置 封装为一个Entry对象:
createEntry(hash, key, value, bucketIndex);
}
//【20】
void createEntry(int hash, K key, V value, int bucketIndex) {
//【21】获取bucketIndex位置上的元素给e
Entry<K,V> e = table[bucketIndex];
//【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
//【23】将新的Entry放在table[bucketIndex]的位置上
table[bucketIndex] = new Entry<>(hash, key, value, e);
//【24】集合中加入一个元素 size+1
size++;
}
//【27】
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//【28】创建长度为newCapacity的数组
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//【28.5】转让方法:将老数组中的东西都重新放入新数组中
transfer(newTable, rehash);
//【29】老数组替换为新数组
table = newTable;
//【29.5】重新计算
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//【28.6】
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
int i = indexFor(e.hash, newCapacity);
//【28.8】头插法
e.next = newTable[i];//获取链表上元素给e.next
newTable[i] = e;//然后将e放在i位置
e = next;//e再指向下一个节点继续遍历
}
}
}
}
细节讲解
- 主数组的长度为2的倍数,
因为这个length的长度,会影响 key的位置:
key的位置的计算:
实际上这个算法就是: h%length ,但是取模的话 效率太低,所以用位运算效率会很高。
原因1:
和等效的前提就是 length必须是2的整数倍
原因2:如果不是2的整数倍,那么 哈西碰撞 哈西冲突的概率就高了很多
位运算 就 涉及 到 length是不是2的整数倍:
比如是2的整数倍:
:
并且这个得到的索引值,一定在 0-15之间(数组是16的时候)
当然如果你扩容后数组长度为 32,那么这个索引就在0-31之间
比如如果不是2的整数倍:
发现:如果不是2的整数倍,那么 哈西碰撞 哈西冲突的概率就高了很多
细节讲解: 装填因子0.75的原因
如果装填因子是1, 那么数组满了再扩容,可以做到 最大的空间利用率
但是这是一个理想状态,元素不可能完全的均匀分布,很可能就哈西碰撞产生链表了。产生链表的话 查询时间就长了。
—》空间好,时间不好
那么有人说 ,把装填因子搞小一点,0.5, 如果是0.5的话,就浪费空间,但是可以做到 到0.5就扩容 ,然后哈西碰撞就少,
不产生链表的话,那么查询效率很高
—》时间好,空间不好
所以在空间和时间中,
取中间值,平衡这个因素 就取值为 0.75
Properties属性
- 类集提供了properties类完成属性操作(properties和xml)
- setProperty
- getProperty
- 将属性保存在文件中: store
- 属性文件读取: load
HashSet底层原理
public class HashSet<E>{
//重要属性:
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
//构造器:
public HashSet() {
map = new HashMap<>();//HashSet底层就是利用HashMap来完成的
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
TreeMap
- 原理大致介绍:
- 源码:
public class TreeMap<K,V>{
//重要属性:
//外部比较器:
private final Comparator<? super K> comparator;
//树的根节点:
private transient Entry<K,V> root = null;
//集合中元素的数量:
private transient int size = 0;
//空构造器:
public TreeMap() {
comparator = null;//如果使用空构造器,那么底层就不使用外部比较器
}
//有参构造器:
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;//如果使用有参构造器,那么就相当于指定了外部比较器
}
public V put(K key, V value) {//k,V的类型在创建对象的时候确定了
//如果放入的是第一对元素,那么t的值为null
Entry<K,V> t = root;//在放入第二个节点的时候,root已经是根节点了
//如果放入的是第一个元素的话,走入这个if中:
if (t == null) {
//自己跟自己比
compare(key, key); // type (and possibly null) check
//根节点确定为root
root = new Entry<>(key, value, null);
//size值变为1
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
//将外部比较器赋给cpr:
Comparator<? super K> cpr = comparator;
//cpr不等于null,意味着你刚才创建对象的时候调用了有参构造器,指定了外部比较器
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);//将元素的key值做比较
//cmp返回的值就是int类型的数据:
//要是这个值《0 =0 》0
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else//cpm==0
//如果key的值一样,那么新的value替换老的value 但是key不变 因为key是唯一的
return t.setValue(value);
} while (t != null);
}
//cpr等于null,意味着你刚才创建对象的时候调用了空构造器,没有指定外部比较器,使用内部比较器
else {
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);//将元素的key值做比较
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;//size加1 操作
modCount++;
return null;
}
}
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left = null;
Entry<K,V> right = null;
Entry<K,V> parent;
boolean color = BLACK;
}
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable{
//重要属性:
private transient NavigableMap<E,Object> m;
private static final Object PRESENT = new Object();
//在调用空构造器的时候,底层创建了一个TreeMap
public TreeSet() {
this(new TreeMap<E,Object>());
}
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
Collections工具集
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
//Collections不支持创建对象,因为构造器私有化了
/*Collections cols = new Collections();*/
//里面的属性和方法都是被static修饰,我们可以直接用类名.去调用即可:
//常用方法:
//addAll:
ArrayList<String> list = new ArrayList<>();
list.add("cc");
list.add("bb");
list.add("aa");
Collections.addAll(list,"ee","dd","ff");
Collections.addAll(list,new String[]{"gg","oo","pp"});
System.out.println(list);
//binarySearch必须在有序的集合中查找:--》排序:
Collections.sort(list);//sort提供的是升序排列
System.out.println(list);
//binarySearch
System.out.println(Collections.binarySearch(list, "cc"));
//copy:替换方法
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2,"tt","ss");
Collections.copy(list,list2);//将list2的内容替换到list上去
System.out.println(list);
System.out.println(list2);
//fill 填充
Collections.fill(list2,"yyy");
System.out.println(list2);
}
}
IO流
File类
文件,目录:
文件:
内存中存放的数据在计算机关机后就会消失。要长久保存数据,就要使用硬盘、光盘、U 盘等设备。为了便于数据的管理和检索,引入了“文件”的概念。一篇文章、一段视频、一个可执行程序,都可以被保存为一个文件,并赋予一个文件名。操作系统以文件为单位管理磁盘中的数据。一般来说,文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别,这是从文件的功能进行分类的。从数据存储的角度来说,所有的文件本质上都是一样的,都是由一个个字节组成的,归根到底都是 0、1 比特串。不同的文件呈现出不同的形态(有的是文本,有的是视频等等)
文件夹(目录):
成千上万个文件如果不加分类放在一起,用户使用起来显然非常不便,因此又引入了树形目录(目录也叫文件夹)的机制,可以把文件放在不同的文件夹中,文件夹中还可以嵌套文件夹,这就便于用户对文件进行管理和使用
在java程序中操纵 文件/目录 ?怎么办?
java程序,最典型的特点,面向对象,java程序最擅长的就是操作对象,盘符上的文件/目录,将它的各种信息进行了封装,封装为一个对象,
java程序最擅长的就是操纵对象,这个对象属于 —》File类盘符上的文件—》封装为对象—》对象属于File类的对象–》有了这个对象,我们程序就可以直接操纵这个对象,通过这个对象获取文件的各种信息,还可以对文件进行创建 ,删除。
对文件进行操作
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//将文件封装为一个File类的对象:
File f = new File("d:\\test.txt");
File f1 = new File("d:\\test.txt");
File f2 = new File("d:/test.txt");
//File.separator属性帮我们获取当前操作系统的路径拼接符号
//在windows,dos下,系统默认用“\”作为路径分隔符 ,在unix,url中,使用“/”作为路径分隔符。
File f3 = new File("d:"+File.separator+"test.txt");//建议使用这种
//常用方法:
System.out.println("文件是否可读:"+f.canRead());
System.out.println("文件是否可写:"+f.canWrite());
System.out.println("文件的名字:"+f.getName());
System.out.println("上级目录:"+f.getParent());
System.out.println("是否是一个目录:"+f.isDirectory());
System.out.println("是否是一个文件:"+f.isFile());
System.out.println("是否隐藏:"+f.isHidden());
System.out.println("文件的大小:"+f.length());
System.out.println("是否存在:"+f.exists());
/*if(f.exists()){//如果文件存在,将文件删除操作
f.delete();
}else{//如果不存在,就创建这个文件
f.createNewFile();
}*/
System.out.println(f == f1);//比较两个对象的地址
System.out.println(f.equals(f1));//比较两个对象对应的文件的路径
//跟路径相关的:
System.out.println("绝对路径:"+f.getAbsolutePath());
System.out.println("相对路径:"+f.getPath());
System.out.println("toString:"+f.toString());
System.out.println("----------------------");
File f5 = new File("demo.txt");
if(!f5.exists()){
f5.createNewFile();
}
//绝对路径指的就是:真实的一个精准的,完整的路径
System.out.println("绝对路径:"+f5.getAbsolutePath());
//相对路径:有一个参照物,相对这个参照物的路径。
//在main方法中,相对位置指的就是:D:\IDEA_workspace\TestJavaSE
//在junit的测试方法中,相对路径指的就是模块位置
System.out.println("相对路径:"+f5.getPath());
//toString的效果永远是 相对路径
System.out.println("toString:"+f5.toString());
File f6 = new File("a/b/c/demo.txt");
if(!f5.exists()){
f5.createNewFile();
}
System.out.println("绝对路径:"+f6.getAbsolutePath());
System.out.println("相对路径:"+f6.getPath());
}
}
对目录进行操作
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//将目录封装为File类的对象:
File f = new File("D:\\IDEA_workspace");
System.out.println("文件是否可读:"+f.canRead());
System.out.println("文件是否可写:"+f.canWrite());
System.out.println("文件的名字:"+f.getName());
System.out.println("上级目录:"+f.getParent());
System.out.println("是否是一个目录:"+f.isDirectory());
System.out.println("是否是一个文件:"+f.isFile());
System.out.println("是否隐藏:"+f.isHidden());
System.out.println("文件的大小:"+f.length());
System.out.println("是否存在:"+f.exists());
System.out.println("绝对路径:"+f.getAbsolutePath());
System.out.println("相对路径:"+f.getPath());
System.out.println("toString:"+f.toString());
//跟目录相关的方法:
File f2 = new File("D:\\a\\b\\c");
//创建目录:
//f2.mkdir();//创建单层目录
//f2.mkdirs();//创建多层目录
//删除:如果是删除目录的话,只会删除一层,并且前提:这层目录是空的,里面没有内容,如果内容就不会被删除
f2.delete();
//查看:
String[] list = f.list();//文件夹下目录/文件对应的名字的数组
for(String s:list){
System.out.println(s);
}
System.out.println("=========================");
File[] files = f.listFiles();//作用更加广泛
for(File file:files){
System.out.println(file.getName()+","+file.getAbsolutePath());
}
}
}
IO
引入
- File类:封装文件/目录的各种信息,对目录/文件进行操作,但是我们不可以获取到文件/目录中的内容。
- 引入:IO流:
I/O : Input/Output的缩写,用于处理设备之间的数据的传输。 - 形象理解:IO流 当做一根 “管”:
- IO流的体系结构:
通过java程序完成文件的复制操作
文件–>程序: FileReader
一个一个字符读取
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//文件--》程序:
//1.有一个文件:----》创建一个File类的对象
File f = new File("d:\\Test.txt");
//2.利用FileReader这个流,这个“管”怼到源文件上去 ---》创建一个FileReader的流的对象
FileReader fr = new FileReader(f);
//3.进行操作“吸”的动作 ---》读取动作
/*下面的代码我们验证了:如果到了文件的结尾处,那么读取的内容为-1
int n1 = fr.read();
int n2 = fr.read();
int n3 = fr.read();
int n4 = fr.read();
int n5 = fr.read();
int n6 = fr.read();
System.out.println(n1);
System.out.println(n2);
System.out.println(n3);
System.out.println(n4);
System.out.println(n5);
System.out.println(n6);*/
//方式1:
/*int n = fr.read();
while(n!=-1){
System.out.println(n);
n = fr.read();
}*/
//方式2:
int n;
while((n = fr.read())!=-1){
System.out.println((char)n);
}
//4.“管”不用了,就要关闭 ---》关闭流
//流,数据库,网络资源,靠jvm本身没有办法帮我们关闭,此时必须程序员手动关闭:
fr.close();
}
}
一次读取五个字符
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//文件--》程序:
//1.创建一个File类的对象
File f = new File("d:\\Test.txt");
//2.创建一个FileReader的流的对象
FileReader fr = new FileReader(f);
//3.读取动作
//引入一个“快递员的小车”,这个“小车”一次拉5个快递:
char[] ch = new char[5];//缓冲数组
int len = fr.read(ch);//一次读取五个:返回值是这个数组中 的有效长度
while(len!=-1){
//System.out.println(len);
//错误方式:
/*for (int i = 0 ;i < ch.length;i++){
System.out.println(ch[i]);
}*/
//正确方式:
/*for (int i = 0 ;i < len;i++){
System.out.println(ch[i]);
}*/
//正确方式2:将数组转为String:
String str = new String(ch,0,len);
System.out.print(str);
len = fr.read(ch);
}
//4.关闭流
fr.close();
}
}
程序–>文件: FileWriter
一个字符一个字符的向外输出
package com.msb.io01;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author : msb-zhaoss
*/
public class Test03 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有个目标文件:
File f = new File("d:\\demo.txt");
//2.FileWriter管怼到文件上去:
FileWriter fw = new FileWriter(f);
//3.开始动作:输出动作:
//一个字符一个字符的往外输出:
String str = "hello你好";
for (int i = 0 ;i < str.length();i++){
fw.write(str.charAt(i));
}
//4.关闭流:
fw.close();
}
}
发现:
如果目标文件不存在的话,那么会自动创建此文件。
如果目标文件存在的话:
new FileWriter(f) 相当于对原文件进行覆盖操作。
new FileWriter(f,false) 相当于对源文件进行覆盖操作。不是追加。
new FileWriter(f,true) 对原来的文件进行追加,而不是覆盖。
利用缓冲数组:向外输出(利用缓冲数组:)
public class Test03 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有个目标文件:
File f = new File("d:\\demo.txt");
//2.FileWriter管怼到文件上去:
FileWriter fw = new FileWriter(f,true);
//3.开始动作:输出动作:
//一个字符一个字符的往外输出:
String str = "你好中国";
char[] chars = str.toCharArray();
fw.write(chars);
//4.关闭流:
fw.close();
}
}
利用FileReader,FileWriter对文件进行复制
public class Test04 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有一个源文件
File f1 = new File("d:\\Test.txt");
//2.有一个目标文件:
File f2 = new File("d:\\Demo.txt");
//3.搞一个输入的管 怼到源文件上:
FileReader fr = new FileReader(f1);
//4.搞一个输出的管,怼到目标文件上:
FileWriter fw = new FileWriter(f2);
//5.开始动作:
//方式1:一个字符一个字符的复制:
/*int n = fr.read();
while(n!=-1){
fw.write(n);
n = fr.read();
}*/
//方式2:利用缓冲字符数组:
/*char[] ch = new char[5];
int len = fr.read(ch);
while(len!=-1){
fw.write(ch,0,len);//将缓冲数组中有效长度写出
len = fr.read(ch);
}*/
//方式3:利用缓冲字符数组,将数组转为String写出。
char[] ch = new char[5];
int len = fr.read(ch);
while(len!=-1){
String s = new String(ch,0,len);
fw.write(s);
len = fr.read(ch);
}
//6.关闭流:(关闭流的时候,倒着关闭,后用先关)
fw.close();
fr.close();
}
}
不要利用字符流去操作非文本文件
文本文件:.txt .java .c .cpp —》建议使用字符流操作
非文本文件:.jpg, .mp3 , .mp4 , .doc , .ppt —》建议使用字节流操作
利用try-catch-finally处理异常方式
public class Test04 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//1.有一个源文件
File f1 = new File("d:\\Test.txt");
//2.有一个目标文件:
File f2 = new File("d:\\Demo.txt");
//3.搞一个输入的管 怼到源文件上:
FileReader fr = null;
FileWriter fw = null;
try {
fr = new FileReader(f1);
//4.搞一个输出的管,怼到目标文件上:
fw = new FileWriter(f2);
//5.开始动作:
char[] ch = new char[5];
int len = fr.read(ch);
while(len!=-1){
String s = new String(ch,0,len);
fw.write(s);
len = fr.read(ch);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//6.关闭流:(关闭流的时候,倒着关闭,后用先关)
try {
if(fw!=null){//防止空指针异常
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(fr!=null){
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileInputStream
读取文本文件
package com.msb.io02;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* @author : msb-zhaoss
*/
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//功能:利用字节流将文件中内容读到程序中来:
//1.有一个源文件:
File f = new File("D:\\Test.txt");
//2.将一个字节流这个管 怼 到 源文件上:
FileInputStream fis = new FileInputStream(f);
//3.开始读取动作
/*
细节1:
文件是utf-8进行存储的,所以英文字符 底层实际占用1个字节
但是中文字符,底层实际占用3个字节。
细节2:
如果文件是文本文件,那么就不要使用字节流读取了,建议使用字符流。
细节3:
read()读取一个字节,但是你有没有发现返回值是 int类型,而不是byte类型?
read方法底层做了处理,让返回的数据都是“正数”
就是为了避免如果字节返回的是-1的话,那到底是读入的字节,还是到文件结尾呢。
*/
int n = fis.read();
while(n!=-1){
System.out.println(n);
n = fis.read();
}
//4.关闭流:
fis.close();
}
}
利用字节流读取非文本文件:(以图片为例) –> 一个一个字节读取
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//功能:利用字节流将文件中内容读到程序中来:
//1.有一个源文件:
File f = new File("D:\\LOL.jpg");
//2.将一个字节流这个管 怼 到 源文件上:
FileInputStream fis = new FileInputStream(f);
//3.开始读取动作
int count = 0;//定义一个计数器,用来计读入的字节的个数
int n = fis.read();
while(n!=-1){
count++;
System.out.println(n);
n = fis.read();
}
System.out.println("count="+count);
//4.关闭流:
fis.close();
}
}
利用字节类型的缓冲数组
public class Test03 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//功能:利用字节流将文件中内容读到程序中来:
//1.有一个源文件:
File f = new File("D:\\LOL.jpg");
//2.将一个字节流这个管 怼 到 源文件上:
FileInputStream fis = new FileInputStream(f);
//3.开始读取动作
//利用缓冲数组:(快递员的小车)
byte[] b = new byte[1024*6];
int len = fis.read(b);//len指的就是读取的数组中的有效长度
while(len!=-1){
//System.out.println(len);
for(int i = 0;i<len;i++){
System.out.println(b[i]);
}
len = fis.read(b);
}
//4.关闭流:
fis.close();
}
}
完成非文本文件的复制
读取一个字节,写出一个字节
public class Test04 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//功能:完成图片的复制:
//1.有一个源图片
File f1 = new File("d:\\LOL.jpg");
//2.有一个目标图片:
File f2 = new File("d:\\LOL2.jpg");
//3.有一个输入的管道 怼 到 源文件:
FileInputStream fis = new FileInputStream(f1);
//4.有一个输出的管道 怼到 目标文件上:
FileOutputStream fos = new FileOutputStream(f2);
//5.开始复制:(边读边写)
int n = fis.read();
while(n!=-1){
fos.write(n);
n = fis.read();
}
//6.关闭流:(倒着关闭流,先用后关)
fos.close();
fis.close();
}
}
利用缓冲字节数组:
public class Test05 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//功能:完成图片的复制:
//1.有一个源图片
File f1 = new File("d:\\LOL.jpg");
//2.有一个目标图片:
File f2 = new File("d:\\LOL2.jpg");
//3.有一个输入的管道 怼 到 源文件:
FileInputStream fis = new FileInputStream(f1);
//4.有一个输出的管道 怼到 目标文件上:
FileOutputStream fos = new FileOutputStream(f2);
//5.开始复制:(边读边写)
//利用缓冲数组:
byte[] b = new byte[1024*8];
int len = fis.read(b);
while(len!=-1){
fos.write(b,0,len);
len = fis.read(b);
}
//6.关闭流:(倒着关闭流,先用后关)
fos.close();
fis.close();
}
}
缓冲字节流(处理流)
BufferedInputStream,BufferedOutputStream
读入一个字节,写出一个字节:
利用缓冲字节数组:
利用缓冲区
想要完成上面的效果,单纯的靠FileInputStream,FileOutputStream是不可以完成的,这个时候就需要功能的加强,
这个加强就需要引入新的流(在FileInputStream,FileOutputStream外面再套一层流):BufferedInputStream ,BufferedOutputStream. —–>处理流
public class Test06 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有一个源图片
File f1 = new File("d:\\LOL.jpg");
//2.有一个目标图片:
File f2 = new File("d:\\LOL2.jpg");
//3.有一个输入的管道 怼 到 源文件:
FileInputStream fis = new FileInputStream(f1);
//4.有一个输出的管道 怼到 目标文件上:
FileOutputStream fos = new FileOutputStream(f2);
//5.功能加强,在FileInputStream外面套一个管:BufferedInputStream:
BufferedInputStream bis = new BufferedInputStream(fis);
//6.功能加强,在FileOutputStream外面套一个管:BufferedOutputStream:
BufferedOutputStream bos = new BufferedOutputStream(fos);
//7.开始动作 :
byte[] b = new byte[1024*6];
int len = bis.read(b);
while(len!=-1){
bos.write(b,0,len);
/* bos.flush(); 底层已经帮我们做了刷新缓冲区的操作,不用我们手动完成:底层调用flushBuffer()*/
len = bis.read(b);
}
//8.关闭流:
//倒着关:
//如果处理流包裹着节点流的话,那么其实只要关闭高级流(处理流),那么里面的字节流也会随之被关闭。
bos.close();
bis.close();
/*fos.close();
fis.close();*/
}
}
对比非文本文件复制的三种方法的效率
【1】读入一个字节,写出一个字节:
【2】利用缓冲字节数组:
【3】利用缓冲区:
代码:
public class Test06 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有一个源图片
File f1 = new File("d:\\LOL.jpg");
//2.有一个目标图片:
File f2 = new File("d:\\LOL2.jpg");
//3.有一个输入的管道 怼 到 源文件:
FileInputStream fis = new FileInputStream(f1);
//4.有一个输出的管道 怼到 目标文件上:
FileOutputStream fos = new FileOutputStream(f2);
//5.功能加强,在FileInputStream外面套一个管:BufferedInputStream:
BufferedInputStream bis = new BufferedInputStream(fis);
//6.功能加强,在FileOutputStream外面套一个管:BufferedOutputStream:
BufferedOutputStream bos = new BufferedOutputStream(fos);
//7.开始动作 :
long startTime = System.currentTimeMillis();
byte[] b = new byte[1024];
int len = bis.read(b);
while(len!=-1){
bos.write(b,0,len);
/* bos.flush(); 底层已经帮我们做了刷新缓冲区的操作,不用我们手动完成:底层调用flushBuffer()*/
len = bis.read(b);
}
long endTime = System.currentTimeMillis();
System.out.println("复制完成的时间为:"+(endTime-startTime));
//8.关闭流:
//倒着关:
//如果处理流包裹着节点流的话,那么其实只要关闭高级流(处理流),那么里面的字节流也会随之被关闭。
bos.close();
bis.close();
/*fos.close();
fis.close();*/
}
}
缓冲字符流(处理流)完成文本文件的复制
public class Test07 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有一个源文件:
File f1 = new File("d:\\Test.txt");
//2.有一个目标文件:
File f2 = new File("d:\\Demo.txt");
//3.需要一个管 怼到 源文件:
FileReader fr = new FileReader(f1);
//4.需要一根管怼到目标文件:
FileWriter fw = new FileWriter(f2);
//5.套一根管在输入字符流外面:
BufferedReader br = new BufferedReader(fr);
//6.套一根管在输出字符流外面:
BufferedWriter bw = new BufferedWriter(fw);
//7.开始动作:
//方式1:读取一个字符,输出一个字符:
/*int n = br.read();
while(n!=-1){
bw.write(n);
n = br.read();
}*/
//方式2:利用缓冲数组:
/*char[] ch = new char[30];
int len = br.read(ch);
while(len!=-1){
bw.write(ch,0,len);
len = br.read(ch);
}*/
//方式3:读取String:
String str = br.readLine();//每次读取文本文件中一行,返回字符串
while(str!=null){
bw.write(str);
//在文本文件中应该再写出一个换行:
bw.newLine();//新起一行
str = br.readLine();
}
//8.关闭流
bw.close();
br.close();
}
}
转换流
InputStreamReader,OutputStreamWriter
- 转换流:作用:将字节流和字符流进行转换。
- 转换流 属于字符流
- InputStreamReader :字节输入流 —》字符的输入流
- OutputStreamWriter : 字符输出流 –》字节的输出流
- 将输入的字节流转换为输入的字符流,然后完成文件–》程序 :
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//文件---》程序:
//1.有一个源文件:
File f = new File("d:\\Test.txt");
//2.需要一个输入的字节流接触文件:
FileInputStream fis = new FileInputStream(f);
//3.加入一个转换流,将字节流转换为字符流:(转换流属于一个处理流)
//将字节转换为字符的时候,需要指定一个编码,这个编码跟文件本身的编码格式统一
//如果编码格式不统一的话,那么在控制台上展示的效果就会出现乱码
//InputStreamReader isr = new InputStreamReader(fis,"utf-8");
//获取程序本身的编码--》utf-8
InputStreamReader isr = new InputStreamReader(fis);
//4.开始动作,将文件中内容显示在控制台:
char[] ch = new char[20];
int len = isr.read(ch);
while(len!=-1){
//将缓冲数组转为字符串在控制台上打印出来
System.out.print(new String(ch,0,len));
len = isr.read(ch);
}
//5.关闭流:
isr.close();
}
}
转换流–>实现文本文件的复制
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.有一个源文件
File f1 = new File("d:\\Test.txt");
//2.有一个目标文件:
File f2 = new File("d:\\Demo.txt");
//3.输入方向:
FileInputStream fis = new FileInputStream(f1);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
//4.输出方向:
FileOutputStream fos = new FileOutputStream(f2);
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
//5.开始动作:
char[] ch = new char[20];
int len = isr.read(ch);
while(len!=-1){
osw.write(ch,0,len);
len = isr.read(ch);
}
//6.关闭流:
osw.close();
isr.close();
}
}
System类对IO流的支持
System的属性:
System.in : “标准”输入流。—》默认情况下 从键盘输入
System.out :“标准”输出流。 —》默认情况下,输出到控制台。
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//得到的是标准的输入流:--》从键盘输入:
//InputStream in = System.in;
//调用方法:
//int n = in.read();//read方法等待键盘的录入,所以这个方法是一个阻塞方法。
//System.out.println(n);
//以前案例:从键盘录入一个int类型的数据:
//从上面的代码证明,键盘录入实际上是:System.in
//形象的理解:System.in管,这个管怼到键盘上去了,所以你从键盘录入的话,就从这个管到程序中了
//Scanner的作用:扫描器:起扫描作用的,扫键盘的从这根管出来的数据
/*Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
System.out.println(i);*/
//既然Scanner是扫描的作用,不一定非得扫 System.in进来的东西,还可以扫描其他管的内容:
Scanner sc = new Scanner(new FileInputStream(new File("d:\\Test.txt")));
while(sc.hasNext()){
System.out.println(sc.next());
}
}
}
System.out:返回的输出流,打印流(PrintStream)
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
//写到控制台:
PrintStream out = System.out;
//调用方法:
out.print("你好1");//直接在控制台写出,但是不换行
out.print("你好2");
out.print("你好3");
out.print("你好4");
out.println("我是中国人1");//直接在控制台写出,并且换行操作
out.println("我是中国人2");
out.println("我是中国人3");
out.println("我是中国人4");
System.out.println("你是");
System.out.print("中国人");
}
}
键盘录入的内容输出到文件中
public class Test03 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.先准备输入方向:
//键盘录入:
InputStream in = System.in;//属于字节流
//字节流--》字符流:
InputStreamReader isr = new InputStreamReader(in);
//在isr外面再套一个缓冲流:
BufferedReader br = new BufferedReader(isr);
//2.再准备输出方向:
//准备目标文件
File f = new File("d:\\Demo1.txt");
FileWriter fw = new FileWriter(f);
BufferedWriter bw = new BufferedWriter(fw);
//3.开始动作:
String s = br.readLine();
while(!s.equals("exit")){
bw.write(s);
bw.newLine();//文件中换行
s = br.readLine();
}
//4.关闭流:
bw.close();
br.close();
}
}
数据流
DataInputStream,DataOutputStream
数据流:用来操作基本数据类型和字符串的
DataInputStream:将文件中存储的基本数据类型和字符串 写入 内存的变量中
DataOutputStream: 将内存中的基本数据类型和字符串的变量 写出 文件中
利用DataOutputStream向外写出变量:
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//DataOutputStream: 将内存中的基本数据类型和字符串的变量 写出 文件中
/*File f = new File("d:\\Demo2.txt");
FileOutputStream fos = new FileOutputStream(f);
DataOutputStream dos = new DataOutputStream(fos);*/
DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File("d:\\Demo2.txt")));
//向外将变量写到文件中去:
dos.writeUTF("你好");
dos.writeBoolean(false);
dos.writeDouble(6.9);
dos.writeInt(82);
//关闭流:
dos.close();
}
}
在Demo2.txt文件中,我们看到:
发现:这个内容我们看不懂,是给程序看的
所以下面我们开始读取的程序:
package com.msb.io05;
import java.io.*;
/**
* @author : msb-zhaoss
*/
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//DataInputStream:将文件中存储的基本数据类型和字符串 写入 内存的变量中
DataInputStream dis = new DataInputStream(new FileInputStream(new File("d:\\Demo2.txt")));
//将文件中内容读取到程序中来:
System.out.println(dis.readUTF());
System.out.println(dis.readBoolean());
System.out.println(dis.readDouble());
System.out.println(dis.readInt());
//关闭流:
dis.close();
}
}
结果:
验证:那个文件,我们看不懂,程序看得懂
要求:
写出的类型跟读入的类型 必须 要匹配!
对象流
ObjectInputStream,ObjectOutputStream
对象流:ObjectInputStream,ObjectInputStream
用于存储和读取基本数据类型数据或对象的处理流。
它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
序列化和反序列化:
ObjectOutputStream 类 : 把内存中的Java对象转换成平台无关的二进制数据,从而允许把这种二进制数据持久地保存在磁盘上,或通过网络将这种二进制数据传输到另一个网络节点。—-》序列化
用ObjectInputStream类 : 当其它程序获取了这种二进制数据,就可以恢复成原来的Java对象。—-》反序列化
代码:操作字符串对象:
首先将一个字符串对象写到文件中去:—-》序列化
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:\\Demo3.txt")));
//将内存中的字符串写出到文件中:
oos.writeObject("你好");
//关闭流:
oos.close();
}
}
查看文件
我们看不懂文件的内容,但是程序是可以看懂的,所以可以写一个程序读文件中内容:—-》反序列化
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException, ClassNotFoundException {
//将文件中保存的字符串 读入到 内存:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\Demo3.txt")));
//读取:
String s = (String)(ois.readObject());
System.out.println(s);
//关闭流:
ois.close();
}
}
控制台:
代码:操作自定义类的对象:
自定义的Person类:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
测试类:
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//序列化:将内存中对象 ---》 文件:
//有一个对象:
Person p = new Person("lili",19);
//有对象流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:\\Demo4.txt")));
//向外写:
oos.writeObject(p);
//关闭流:
oos.close();
}
}
运行的时候发现出现异常:
出现异常的原因:==你想要序列化的那个对象对应的类,必须要实现一个接口==:
接口内部,什么都没有,这种接口叫 标识接口。 起到标识作用,标识什么呢?只要实现这个接口的类的对象才能序列化,否则不可以。
解决办法:将Person 实现这个标识接口就可以:
public class Person implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
测试:发现序列化成功,Person具备了序列化的能力。
这个二进制数据我们看不懂,但是程序可以看懂,所以我们可以用程序实现 反序列化操作:
将这个对象 恢复到内存中来:
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\Demo4.txt")));
//读入内存:
Person p = (Person)(ois.readObject());
System.out.println(p/*.toString()*/);
//关闭流:
ois.close();
}
}
结果:
因为我们没有重写toString方法,所以结果为:
证明了反序列化成功: 将二进制数据 –》内存
serialVersionUID:
凡是实现Serializable接口(标识接口)的类都有一个表示序列化版本标识符的静态常量:
➢private static final long serialVersionUID;
➢serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序加化时是否兼容。
➢如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议,显式声明。➢简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)
添加Person类中加入toString方法:
再次运行测试类:
出现异常:
出现异常的原因:
解决:给这个类 加入一个 序列号:serialVersionUID
IDEA中配置序列化版本号:
在Person类上:alt+enter:
回车即可生成
- 序列化细节:
- 被序列化的类的内部的所有属性,必须是可序列化的 (基本数据类型都是可序列化的)
- static,transient修饰的属性 不可以被序列化。
public class Person implements Serializable {
private static final long serialVersionUID = 8027651838638826533L;
private transient String name;
private static int age;
private Famaily f = new Famaily();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", f=" + f + ",age=" + age +
'}';
}
}
结果:
多线程
1. 程序,进程,线程
【1】程序,进程,线程
➢程序(program):是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)
➢进程(process):是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程
➢线程(thread),进程可进一步细化为线程, 是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的。
【2】单核CPU与多核CPU的任务执行:
【3】并行和并发:
并行:多个CPU同时执行多个任务
并发:一个CPU“同时”执行多个任务(采用时间片切换)
2. 创建线程的三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
对比第一种和第二种创建线程的方式发现,无论第一种继承Thread类的方式还是第二种实现Runnable接口的方式,都需要有一个run方法,
但是这个run方法有不足:
(1)没有返回值
(2)不能抛出异常基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现Callable接口:
实现Callable接口好处:(1)有返回值 (2)能抛出异常
缺点:线程创建比较麻烦
线程的声明周期
【1】线程声明周期:线程开始–》线程消亡
【2】线程经历哪些阶段:
【1】同优先级别的线程,采取的策略就是先到先服务,使用时间片策略
【2】如果优先级别高,被CPU调度的概率就高
【3】级别:1-10 默认的级别为5
【4】代码:
package com.msb.test06;
/**
* @author : msb-zhaoss
*/
public class TestThread01 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
class TestThread02 extends Thread{
@Override
public void run() {
for (int i = 20; i <= 30 ; i++) {
System.out.println(i);
}
}
}
class Test{
//这是main方法,程序的入口
public static void main(String[] args) {
//创建两个子线程,让这两个子线程争抢资源:
TestThread01 t1 = new TestThread01();
t1.setPriority(10);//优先级别高
t1.start();
TestThread02 t2 = new TestThread02();
t2.setPriority(1);//优先级别低
t2.start();
}
}
join
join方法:当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。
注意:必须先start,再join才有效
。
sleep
【1】sleep : 人为的制造阻塞事件
public class Test01 {
//这是main方法,程序的入口
public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("00000000000000");
}
}
setDaemon设置伴随线程
【1】设置伴随线程
将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了
案例:皇上 –》驾崩 —》妃子陪葬
package com.msb.test09;
/**
* @author : msb-zhaoss
*/
public class TestThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 1000 ; i++) {
System.out.println("子线程----"+i);
}
}
}
class Test{
//这是main方法,程序的入口
public static void main(String[] args) {
//创建并启动子线程:
TestThread tt = new TestThread();
tt.setDaemon(true);//设置伴随线程 注意:先设置,再启动
tt.start();
//主线程中还要输出1-10的数字:
for (int i = 1; i <= 10 ; i++) {
System.out.println("main---"+i);
}
}
}
stop
package com.msb.test09;
/**
* @author : msb-zhaoss
*/
public class Demo {
//这是main方法,程序的入口
public static void main(String[] args) {
for (int i = 1; i <= 100 ; i++) {
if(i == 6){
Thread.currentThread().stop();//过期方法,不建议使用
}
System.out.println(i);
}
}
}
线程安全问题
1. 同步代码块
package com.msb.test04;
/**
* @author : msb-zhaoss
*/
public class BuyTicketThread implements Runnable {
int ticketNum = 10;
@Override
public void run() {
//此处有1000行代码
for (int i = 1; i <= 100 ; i++) {
synchronized (this){//把具有安全隐患的代码锁住即可,如果锁多了就会效率低 --》this就是这个锁
if(ticketNum > 0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
}
}
}
//此处有1000行代码
}
}
public class BuyTicketThread extends Thread {
public BuyTicketThread(String name){
super(name);
}
//一共10张票:
static int ticketNum = 10;//多个对象共享10张票
//每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票:
for (int i = 1; i <= 100 ; i++) {
synchronized (BuyTicketThread.class){//锁必须多个线程用的是同一把锁!!!
if(ticketNum > 0){//对票数进行判断,票数大于零我们才抢票
System.out.println("我在"+this.getName()+"买到了从北京到哈尔滨的第" + ticketNum-- + "张车票");
}
}
}
}
}
【3】同步监视器总结:
总结1:认识同步监视器(锁子) —– synchronized(同步监视器){ }
1)必须是引用数据类型,不能是基本数据类型
2)也可以创建一个专门的同步监视器,没有任何业务含义
3)一般使用共享资源做同步监视器即可
4)在同步代码块中不能改变同步监视器对象的引用5)尽量不要String和包装类Integer做同步监视器
6)建议使用final修饰同步监视器总结2:同步代码块的执行过程
1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)总结3:其他
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
2. 同步方法
public class BuyTicketThread implements Runnable {
int ticketNum = 10;
@Override
public void run() {
//此处有1000行代码
for (int i = 1; i <= 100 ; i++) {
buyTicket();
}
//此处有1000行代码
}
public synchronized void buyTicket(){//锁住的是this
if(ticketNum > 0){
System.out.println("我在"+Thread.currentThread().getName()+"买到了北京到哈尔滨的第" + ticketNum-- + "张车票");
}
}
}
public class BuyTicketThread extends Thread {
public BuyTicketThread(String name){
super(name);
}
//一共10张票:
static int ticketNum = 10;//多个对象共享10张票
//每个窗口都是一个线程对象:每个对象执行的代码放入run方法中
@Override
public void run() {
//每个窗口后面有100个人在抢票:
for (int i = 1; i <= 100 ; i++) {
buyTicket();
}
}
public static synchronized void buyTicket(){//锁住的 同步监视器: BuyTicketThread.class
if(ticketNum > 0){//对票数进行判断,票数大于零我们才抢票
System.out.println("我在"+Thread.currentThread().getName()+"买到了从北京到哈尔滨的第" + ticketNum-- + "张车票");
}
}
}
【2】总结:
总结1:
多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。
咱们的锁一般都是引用数据类型的。目的:解决了线程安全问题。
总结2:关于同步方法
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this
静态同步方法的同步监视器是 类名.class 字节码信息对象- 同步代码块的效率要高于同步方法
原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
3. Lock锁
【1】Lock锁引入:
JDK1.5后新增新一代的线程同步方式:Lock锁
与采用synchronized相比,lock可提供多种锁方案,更灵活synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。【3】 Lock和synchronized的区别
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
3.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)【4】优先使用顺序:
Lock—-同步代码块(已经进入了方法体,分配了相应资源)—-同步方法(在方法体之外)
线程同步的优缺点
【1】对比:
线程安全,效率低
线程不安全,效率高【2】可能造成死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续[3]代码
public class TestDeadLock implements Runnable { public int flag = 1; static Object o1 = new Object(),o2 = new Object(); public void run(){ System.out.println("flag=" + flag); // 当flag==1锁住o1 if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } // 只要锁住o2就完成 synchronized (o2) { System.out.println("2"); } } } // 如果flag==0锁住o2 if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } // 只要锁住o1就完成 synchronized (o1) { System.out.println("3"); } } } } public static void main(String[] args) { // 实例2个线程类 TestDeadLock td1 = new TestDeadLock(); TestDeadLock td2 = new TestDeadLock(); td1.flag = 1; td2.flag = 0; // 开启2个线程 Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); } }
【4】解决方法: 减少同步资源的定义,避免嵌套同步
线程通信问题
出现问题:
1.生产者和消费者没有交替输出
2.打印数据错乱
哈尔滨 - null
费列罗啤酒
哈尔滨巧克力
package com.msb.test11;
/**
* @author : msb-zhaoss
*/
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//引入一个灯:true:红色 false 绿色
boolean flag = false;//默认情况下没有商品 让生产者先生产 然后消费者再消费
//setter,getter方法;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//生产商品
public synchronized void setProduct(String brand,String name){
if(flag == true){//灯是红色,证明有商品,生产者不生产,等着消费者消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//灯是绿色的,就生产:
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
//将生产信息做一个打印:
System.out.println("生产者生产了:" + this.getBrand() + "---" + this.getName());
//生产完以后,灯变色:变成红色:
flag = true;
//告诉消费者赶紧来消费:
notify();
}
//消费商品:
public synchronized void getProduct(){
if(!flag){//flag == false没有商品,等待生产者生产:
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有商品,消费:
System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());
//消费完:灯变色:
flag = false;
//通知生产者生产:
notify();
}
}
- 原理:
注意:wait方法和notify方法 是必须放在同步方法或者同步代码块中才生效的 (因为在同步的基础上进行线程的通信才是有效的)
注意:sleep和wait的区别:sleep进入阻塞状态没有释放锁,wait进入阻塞状态但是同时释放了锁
- 程生命周期完整图:
Condition是在Java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
它的更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition
一个Condition包含一个等待队列。一个Lock可以产生多个Condition,所以可以有多个等待队列。
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而Lock(同步器)拥有一个同步队列和多个等待队列。Object中的wait(),notify(),notifyAll()方法是和”同步锁”(synchronized关键字)捆绑使用的;而Condition是需要与”互斥锁”/“共享锁”捆绑使用的。
调用Condition的await()、signal()、signalAll()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
- Conditon中的await()对应Object的wait();
- Condition中的signal()对应Object的notify();
- Condition中的signalAll()对应Object的notifyAll()。
void await() throws InterruptedException
造成当前线程在接到信号或被中断之前一直处于等待状态。
与此 Condition 相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下四种情况之一 以前,当前线程将一直处于休眠状态:
- 其他某个线程调用此 Condition 的 signal() 方法,并且碰巧将当前线程选为被唤醒的线程;
- 或者其他某个线程调用此 Condition 的 signalAll() 方法;
- 或者其他某个线程中断当前线程,且支持中断线程的挂起;
- 或者发生“虚假唤醒”
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证它保持此锁。
void signal()
唤醒一个等待线程。
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁.
void signalAll()
- 唤醒所有等待线程。
- 如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
package com.msb.test12;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author : msb-zhaoss
*/
public class Product {//商品类
//品牌
private String brand;
//名字
private String name;
//声明一个Lock锁:
Lock lock = new ReentrantLock();
//搞一个生产者的等待队列:
Condition produceCondition = lock.newCondition();
//搞一个消费者的等待队列:
Condition consumeCondition = lock.newCondition();
//引入一个灯:true:红色 false 绿色
boolean flag = false;//默认情况下没有商品 让生产者先生产 然后消费者再消费
//setter,getter方法;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//生产商品
public void setProduct(String brand,String name){
lock.lock();
try{
if(flag == true){//灯是红色,证明有商品,生产者不生产,等着消费者消费
try {
//wait();
//生产者阻塞,生产者进入等待队列中
produceCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//灯是绿色的,就生产:
this.setBrand(brand);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
//将生产信息做一个打印:
System.out.println("生产者生产了:" + this.getBrand() + "---" + this.getName());
//生产完以后,灯变色:变成红色:
flag = true;
//告诉消费者赶紧来消费:
//notify();
consumeCondition.signal();
}finally {
lock.unlock();
}
}
//消费商品:
public void getProduct(){
lock.lock();
try{
if(!flag){//flag == false没有商品,等待生产者生产:
try {
// wait();
//消费者等待,消费者线程进入等待队列:
consumeCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//有商品,消费:
System.out.println("消费者消费了:" + this.getBrand() + "---" + this.getName());
//消费完:灯变色:
flag = false;
//通知生产者生产:
//notify();
produceCondition.signal();
}finally {
lock.unlock();
}
}
}
网络编程
引入
【1】网络编程:
把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。
设备之间在网络中进行数据的传输,发送/接收数据。
【2】通信两个重要的要素:IP+PORT
【3】设备之间进行传输的时候,必须遵照一定的规则 —》通信协议:
【4】TCP协议:可靠的
【5】UDP协议:不可靠的
InetAddress,InetSocketAdress
前情提要:File —》 封装盘符一个文件
【1】InetAddress —》 封装了IP
public class Test01 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws UnknownHostException {
//封装IP:
//InetAddress ia = new InetAddress();不能直接创建对象,因为InetAddress()被default修饰了。
InetAddress ia = InetAddress.getByName("192.168.199.217");
System.out.println(ia);
InetAddress ia2 = InetAddress.getByName("localhost");//localhost指代的是本机的ip地址
System.out.println(ia2);
InetAddress ia3 = InetAddress.getByName("127.0.0.1");//127.0.0.1指代的是本机的ip地址
System.out.println(ia3);
InetAddress ia4 = InetAddress.getByName("LAPTOP-CRIVSRRU");//封装计算机名
System.out.println(ia4);
InetAddress ia5 = InetAddress.getByName("www.mashibing.com");//封装域名
System.out.println(ia5);
System.out.println(ia5.getHostName());//获取域名
System.out.println(ia5.getHostAddress());//获取ip地址
}
}
【2】InetSocketAddress —》封装了IP,端口号
public class Test02 {
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
InetSocketAddress isa = new InetSocketAddress("192.168.199.217",8080);
System.out.println(isa);
System.out.println(isa.getHostName());
System.out.println(isa.getPort());
InetAddress ia = isa.getAddress();
System.out.println(ia.getHostName());
System.out.println(ia.getHostAddress());
}
}
网络通信原理–>套接字
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
socket(套接字: 通信的一个客户端,称为TCP通信)
比如: 使用飞秋客户端
网址: 协议http和域名组成的
http协议分为两种: tcp协议和udp协议
datgramsocket(套接字: 通信的一个基站,称为udp通信)
- 拜访方式GET/POST是TCP/UDP协议进一步分解的协议
- tcp和udp协议的区别
- tcp协议数据安全性较高,丢失比较少,响应机制称为确认关系,三次握手(getResponseCode())
- udp协议一般称为数据的安全性是不可靠的,可能存在数据丢失,没有响应机制,没有确认关系,一次握手
- 命令行使用”netstat -a/-an”(all 所有的)
协议 本地地址 外部地址 监听
TCP 0.0.0.0(内网ip):445(端口port) WIN-…(万网IP:白名单):0 LISTENING
socket和datagramsocket都是即时通讯工具,不是一个标准的http协议
socket需要加上很多协议才能编程一个完整的tcp协议
datagramsocket也是需要加上很多协议变成完整的udp协议
将socket和datagramsocket称为局域网通信
将tcp和udp称为广域网通信(外网通信)
java中所有的网络通信的包都在java.net包下面
基于TCP的网络编程
功能:模拟网站的登录,客户端录入账号密码,然后服务器端进行验证。
单项通信:
功能:客户端发送一句话到服务器:
客户端:
public class TestClient {//客户端
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.创建套接字:指定服务器的ip和端口号:
Socket s = new Socket("192.168.199.217",8888);
//2.对于程序员来说,向外发送数据 感受 --》利用输出流:
OutputStream os = s.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
//利用这个OutputStream就可以向外发送数据了,但是没有直接发送String的方法
//所以我们又在OutputStream外面套了一个处理流:DataOutputStream
dos.writeUTF("你好");
//3.关闭流 + 关闭网络资源:
dos.close();
os.close();
s.close();
}
}
服务器
public class TestServer {//服务器
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.创建套接字: 指定服务器的端口号
ServerSocket ss = new ServerSocket(8888);
//2.等着客户端发来的信息:
Socket s = ss.accept();//阻塞方法:等待接收客户端的数据,什么时候接收到数据,什么时候程序继续向下执行。
//accept()返回值为一个Socket,这个Socket其实就是客户端的Socket
//接到这个Socket以后,客户端和服务器才真正产生了连接,才真正可以通信了
//3.感受到的操作流:
InputStream is = s.getInputStream();
DataInputStream dis = new DataInputStream(is);
//4.读取客户端发来的数据:
String str = dis.readUTF();
System.out.println("客户端发来的数据为:"+str);
//5.关闭流+关闭网络资源:
dis.close();
is.close();
s.close();
ss.close();
}
}
双向通信:
服务器端
package com.msb.test02;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author : msb-zhaoss
*/
public class TestServer {//服务器
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.创建套接字: 指定服务器的端口号
ServerSocket ss = new ServerSocket(8888);
//2.等着客户端发来的信息:
Socket s = ss.accept();//阻塞方法:等待接收客户端的数据,什么时候接收到数据,什么时候程序继续向下执行。
//accept()返回值为一个Socket,这个Socket其实就是客户端的Socket
//接到这个Socket以后,客户端和服务器才真正产生了连接,才真正可以通信了
//3.感受到的操作流:
InputStream is = s.getInputStream();
DataInputStream dis = new DataInputStream(is);
//4.读取客户端发来的数据:
String str = dis.readUTF();
System.out.println("客户端发来的数据为:"+str);
//向客户端输出一句话:---》操作流---》输出流
OutputStream os = s.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("你好,我是服务器端,我接受到你的请求了");
//5.关闭流+关闭网络资源:
dos.close();
os.close();
dis.close();
is.close();
s.close();
ss.close();
}
}
客户端:
package com.msb.test02;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author : msb-zhaoss
*/
public class TestServer {//服务器
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.创建套接字: 指定服务器的端口号
ServerSocket ss = new ServerSocket(8888);
//2.等着客户端发来的信息:
Socket s = ss.accept();//阻塞方法:等待接收客户端的数据,什么时候接收到数据,什么时候程序继续向下执行。
//accept()返回值为一个Socket,这个Socket其实就是客户端的Socket
//接到这个Socket以后,客户端和服务器才真正产生了连接,才真正可以通信了
//3.感受到的操作流:
InputStream is = s.getInputStream();
DataInputStream dis = new DataInputStream(is);
//4.读取客户端发来的数据:
String str = dis.readUTF();
System.out.println("客户端发来的数据为:"+str);
//向客户端输出一句话:---》操作流---》输出流
OutputStream os = s.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF("你好,我是服务器端,我接受到你的请求了");
//5.关闭流+关闭网络资源:
dos.close();
os.close();
dis.close();
is.close();
s.close();
ss.close();
}
}
对象流传送
封装的user类
package com.msb.test03;
import java.io.Serializable;
/**
* @author : msb-zhaoss
*/
public class User implements Serializable {
private static final long serialVersionUID = 9050691344308365540L;
private String name;
private String pwd;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
}
客户端
package com.msb.test03;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* @author : msb-zhaoss
*/
public class TestClient {//客户端
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
//1.创建套接字:指定服务器的ip和端口号:
Socket s = new Socket("192.168.199.217",8888);
//录入用户的账号和密码:
Scanner sc = new Scanner(System.in);
System.out.println("请录入您的账号:");
String name = sc.next();
System.out.println("请录入您的密码:");
String pwd = sc.next();
//将账号和密码封装为一个User的对象:
User user = new User(name,pwd);
//2.对于程序员来说,向外发送数据 感受 --》利用输出流:
OutputStream os = s.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(user);
//接收服务器端的回话--》利用输入流:
InputStream is = s.getInputStream();
DataInputStream dis = new DataInputStream(is);
boolean b = dis.readBoolean();
if(b){
System.out.println("恭喜,登录成功");
}else{
System.out.println("对不起,登录失败");
}
//3.关闭流 + 关闭网络资源:
dis.close();
is.close();
oos.close();
os.close();
s.close();
}
}
服务器
package com.msb.test03;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author : msb-zhaoss
*/
public class TestServer {//服务器
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.创建套接字: 指定服务器的端口号
ServerSocket ss = new ServerSocket(8888);
//2.等着客户端发来的信息:
Socket s = ss.accept();//阻塞方法:等待接收客户端的数据,什么时候接收到数据,什么时候程序继续向下执行。
//accept()返回值为一个Socket,这个Socket其实就是客户端的Socket
//接到这个Socket以后,客户端和服务器才真正产生了连接,才真正可以通信了
//3.感受到的操作流:
InputStream is = s.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is);
//4.读取客户端发来的数据:
User user = (User)(ois.readObject());
//对对象进行验证:
boolean flag = false;
if(user.getName().equals("娜娜")&&user.getPwd().equals("123123")){
flag = true;
}
//向客户端输出结果:---》操作流---》输出流
OutputStream os = s.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeBoolean(flag);
//5.关闭流+关闭网络资源:
dos.close();
os.close();
ois.close();
is.close();
s.close();
ss.close();
}
}
多线程接收用户请求
遗留问题:服务器针对一个请求服务,之后服务器就关闭了(程序自然结束了)
现在需要解决:服务器必须一直在监听 ,一直开着,等待客户端的请求
在当前代码中,客户端不用动了
更改服务器代码:
package com.msb.test03;
import java.io.*;
import java.net.Socket;
/**
* @author : msb-zhaoss
*/
public class ServerThread extends Thread {//线程:专门处理客户端的请求
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
DataOutputStream dos = null;
Socket s = null;
public ServerThread(Socket s){
this.s = s;
}
@Override
public void run() {
try{
//2.等着客户端发来的信息:
is = s.getInputStream();
ois = new ObjectInputStream(is);
//4.读取客户端发来的数据:
User user = (User)(ois.readObject());
//对对象进行验证:
boolean flag = false;
if(user.getName().equals("娜娜")&&user.getPwd().equals("123123")){
flag = true;
}
//向客户端输出结果:---》操作流---》输出流
os = s.getOutputStream();
dos = new DataOutputStream(os);
dos.writeBoolean(flag);
}catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
try {
if(dos!=null){
dos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(os!=null){
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(ois!=null){
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(is!=null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class TestServer {//服务器
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
System.out.println("服务器启动了");
//1.创建套接字: 指定服务器的端口号
ServerSocket ss = null;
Socket s = null;
int count = 0;//定义一个计数器,用来计数 客户端的请求
try {
ss = new ServerSocket(8888);
while(true){//加入死循环,服务器一直监听客户端是否发送数据
s = ss.accept();//阻塞方法:等待接收客户端的数据,什么时候接收到数据,什么时候程序继续向下执行。
//每次过来的客户端的请求 靠 线程处理:
new ServerThread(s).start();
count++;
//输入请求的客户端的信息:
System.out.println("当前是第"+count+"个用户访问我们的服务器,对应的用户是:"+s.getInetAddress());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
基于UDP的网路通信
TCP:
客户端:Socket –> 程序感受到的 使用流 :输出流
服务器端: ServerSocket –>Socket 程序感受到的 使用流 :输入流
(客户端和服务器端地位不平等。)
UDP:
发送方:DatagramSocket 发送:数据包 DatagramPacket
接收方:DatagramSocket 接收:数据包 DatagramPacket
(发送方和接收方的地址是平等的。)
UDP案例: 完成网站的咨询聊天
一般来实现UDP协议之前都先实现TCP协议
功能分解一:
发送方:
public class TestSend {//发送方:
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
System.out.println("学生上线。。。");
//1.准备套接字: 指定发送方的端口号
DatagramSocket ds = new DatagramSocket(8888);
//2.准备数据包
String str = "你好";
byte[] bytes = str.getBytes();
/*
需要四个参数:
1.指的是传送数据转为字节数组
2.字节数组的长度
3.封装接收方的ip
4.指定接收方的端口号
*/
DatagramPacket dp = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),9999);
//发送:
ds.send(dp);
//关闭资源
ds.close();
}
}
接收方
public class TestReceive {//接收方
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
System.out.println("老师上线了。。");
//1.创建套接字:指定接收方的端口
DatagramSocket ds = new DatagramSocket(9999);
//2.有一个空的数据包,打算用来接收 对方传过来的数据包:
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b,b.length);
//3.接收对方的数据包,然后放入我们的dp数据包中填充
ds.receive(dp);//接收完以后 dp里面就填充好内容了
//4.取出数据:
byte[] data = dp.getData();
String s = new String(data,0,dp.getLength());//dp.getLength()数组包中的有效长度
System.out.println("学生对我说:"+s);
//5.关闭资源:
ds.close();
}
}
功能分解二:
发送方
public class TestSend {//发送方:
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
System.out.println("学生上线。。。");
//1.准备套接字: 指定发送方的端口号
DatagramSocket ds = new DatagramSocket(8888);
//2.准备数据包
Scanner sc = new Scanner(System.in);
System.out.print("学生:");
String str = sc.next();
byte[] bytes = str.getBytes();
/*
需要四个参数:
1.指的是传送数据转为Z字节数组
2.字节数组的长度
3.封装接收方的ip
4.指定接收方的端口号
*/
DatagramPacket dp = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),9999);
//发送:
ds.send(dp);
//接收老师发送回来的信息:
byte[] b = new byte[1024];
DatagramPacket dp2 = new DatagramPacket(b,b.length);
ds.receive(dp2);//接收完以后 dp2里面就填充好内容了
//取出数据:
byte[] data = dp2.getData();
String s = new String(data,0,dp2.getLength());//dp.getLength()数组包中的有效长度
System.out.println("老师对我说:"+s);
//关闭资源
ds.close();
}
}
接收方:
public class TestReceive {//接收方
//这是一个main方法,是程序的入口:
public static void main(String[] args) throws IOException {
System.out.println("老师上线了。。");
//1.创建套接字:指定接收方的端口
DatagramSocket ds = new DatagramSocket(9999);
//2.有一个空的数据包,打算用来接收 对方传过来的数据包:
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b,b.length);
//3.接收对方的数据包,然后放入我们的dp数据包中填充
ds.receive(dp);//接收完以后 dp里面就填充好内容了
//4.取出数据:
byte[] data = dp.getData();
String s = new String(data,0,dp.getLength());//dp.getLength()数组包中的有效长度
System.out.println("学生对我说:"+s);
//老师进行回复:
Scanner sc = new Scanner(System.in);
System.out.print("老师:");
String str = sc.next();
byte[] bytes = str.getBytes();
//封装数据,并且指定学生的ip和端口号
DatagramPacket dp2 = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),8888);
//发送:
ds.send(dp2);
//5.关闭资源:
ds.close();
}
}
功能分解:
发送方
public class TestSend {//发送方:
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
System.out.println("学生上线。。。");
//1.准备套接字: 指定发送方的端口号
DatagramSocket ds = null;
try {
ds = new DatagramSocket(8888);
//2.准备数据包
Scanner sc = new Scanner(System.in);
System.out.print("学生:");
String str = sc.next();
byte[] bytes = str.getBytes();
/*
需要四个参数:
1.指的是传送数据转为Z字节数组
2.字节数组的长度
3.封装接收方的ip
4.指定接收方的端口号
*/
DatagramPacket dp = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),9999);
//发送:
ds.send(dp);
//接收老师发送回来的信息:
byte[] b = new byte[1024];
DatagramPacket dp2 = new DatagramPacket(b,b.length);
ds.receive(dp2);//接收完以后 dp2里面就填充好内容了
//取出数据:
byte[] data = dp2.getData();
String s = new String(data,0,dp2.getLength());//dp.getLength()数组包中的有效长度
System.out.println("老师对我说:"+s);
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
ds.close();
}
}
}
接收方:
public class TestReceive {//接收方
//这是一个main方法,是程序的入口:
public static void main(String[] args){
System.out.println("老师上线了。。");
//1.创建套接字:指定接收方的端口
DatagramSocket ds = null;
try {
ds = new DatagramSocket(9999);
//2.有一个空的数据包,打算用来接收 对方传过来的数据包:
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b,b.length);
//3.接收对方的数据包,然后放入我们的dp数据包中填充
ds.receive(dp);//接收完以后 dp里面就填充好内容了
//4.取出数据:
byte[] data = dp.getData();
String s = new String(data,0,dp.getLength());//dp.getLength()数组包中的有效长度
System.out.println("学生对我说:"+s);
//老师进行回复:
Scanner sc = new Scanner(System.in);
System.out.print("老师:");
String str = sc.next();
byte[] bytes = str.getBytes();
//封装数据,并且指定学生的ip和端口号
DatagramPacket dp2 = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),8888);
//发送:
ds.send(dp2);
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//5.关闭资源:
ds.close();
}
}
}
功能分解4:
发送方:
public class TestSend {//发送方:
//这是一个main方法,是程序的入口:
public static void main(String[] args) {
System.out.println("学生上线。。。");
//1.准备套接字: 指定发送方的端口号
DatagramSocket ds = null;
try {
ds = new DatagramSocket(8888);
while(true){
//2.准备数据包
Scanner sc = new Scanner(System.in);
System.out.print("学生:");
String str = sc.next();
byte[] bytes = str.getBytes();
/*
需要四个参数:
1.指的是传送数据转为Z字节数组
2.字节数组的长度
3.封装接收方的ip
4.指定接收方的端口号
*/
DatagramPacket dp = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),9999);
//发送:
ds.send(dp);
if(str.equals("byebye")){
System.out.println("学生下线。。");
break;
}
//接收老师发送回来的信息:
byte[] b = new byte[1024];
DatagramPacket dp2 = new DatagramPacket(b,b.length);
ds.receive(dp2);//接收完以后 dp2里面就填充好内容了
//取出数据:
byte[] data = dp2.getData();
String s = new String(data,0,dp2.getLength());//dp.getLength()数组包中的有效长度
System.out.println("老师对我说:"+s);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
ds.close();
}
}
}
接收方:
public class TestReceive {//接收方
//这是一个main方法,是程序的入口:
public static void main(String[] args){
System.out.println("老师上线了。。");
//1.创建套接字:指定接收方的端口
DatagramSocket ds = null;
try {
ds = new DatagramSocket(9999);
while(true){
//2.有一个空的数据包,打算用来接收 对方传过来的数据包:
byte[] b = new byte[1024];
DatagramPacket dp = new DatagramPacket(b,b.length);
//3.接收对方的数据包,然后放入我们的dp数据包中填充
ds.receive(dp);//接收完以后 dp里面就填充好内容了
//4.取出数据:
byte[] data = dp.getData();
String s = new String(data,0,dp.getLength());//dp.getLength()数组包中的有效长度
System.out.println("学生对我说:"+s);
if(s.equals("byebye")){
System.out.println("学生已经下线了,老师也下线。。。");
break;
}
//老师进行回复:
Scanner sc = new Scanner(System.in);
System.out.print("老师:");
String str = sc.next();
byte[] bytes = str.getBytes();
//封装数据,并且指定学生的ip和端口号
DatagramPacket dp2 = new DatagramPacket(bytes,bytes.length, InetAddress.getByName("localhost"),8888);
//发送:
ds.send(dp2);
}
} catch (SocketException e) {
e.printStackTrace();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//5.关闭资源:
ds.close();
}
}
}