多态
多态方法具有多种形态,建立在封装和继承上,类(对象)存在继承关系,多态的向上转型与向下转型
1.一个对象的编译类型可以和运行类型不一致,java的动态绑定机制线运行在属性
2.编译类型在定义对象时已经定了,不能更改
3.运行类型可以改变,是变换的。
4.编译类型看“=”左边,运行类型看“=”右边。
向上转型:本质:父类引用指向子类对象,向上转型是自动的
特点“编左运右”可调用父类所有成员(权限)不调用子类的特有成员,最终看子类的具体实现。
语法:父类类型 引用 = new 子类类型()
向下转型:父类引用必须指向当前目标类型对象,只能强转父类引用不能强转父类对象,向下转型要强转
语法:子类类型 引用名 = (子类类型)父类引用
可调用子类所有方法
Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用和接口引用调用的实现则有所不同。总体而言,当某个方法被调用时,JVM 首先要查找相应的常量池,得到方法的符号引用,并查找调用类的方法表以确定该方法的直接引用,最后才真正调用该方法。以下分别对该过程中涉及到的相关部分做详细介绍。
动态绑定:JVM有个方法表:记录当前类以及所有父类的可见方法字节码在内存中的直接地址。
封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
好处
-
减少耦合:可以独立地开发、测试、优化、使用、理解和修改
-
减轻维护的负担:可以更容易被理解,并且在调试的时候可以不影响其他模块
-
有效地调节性能:可以通过剖析来确定哪些模块影响了系统的性能
-
提高软件的可重用性
-
降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
hashCode
1.提高容器效率
2.两个引用指向一个对象,则hashcode值一样
3.不同对象不同哈希值
4.哈希值根据地址号来
5.
6.后面集合需要可以重写hashCode
继承
优点:提高代码的复用性,代码扩展和可维护性提高
细节:
-
子类继承了所有属性,方法,非私有属性,方法子类可以直接访问,私有属性不能在子类直接访问需要提供相应的方法
-
子类必须调用父类的构造器,完成父类的初始化
-
创建子类对象不管调用子类的那个构造器,默认调用父类的构造器完成对父类的初始化,在子类中调用super去指向父类,否则编译不会通过。
-
如果指定调用父类的构造器,显示调用
-
super调用放在第一行
-
super和this只能放在第一行,两个不能同时出现在同一构造器
-
java所有类是Object的子类
-
父类不限于直接父类也可以调用Object类中
-
子类最多只能继承一个父类,单继承
-
不能滥用继承,必须满足is-a关系
缺点
-
父类变,子类就必须变。
-
继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的。
-
继承是一种强耦合关系。
super和this
区别:
访问属性 | 调用方法 | 构造器 | 特殊 | |
---|---|---|---|---|
this | 先本类属性,没有从父类中查找 | 先本类方法,没有从父类查找 | 先调用本类,放在首行 | 当前对象 |
super | 访问查找父类 | 访问父类 | 调用父类构造器放在子类构造首行 | 子类访问父类对象 |
内存泄露
内存泄露的场景:
使用静态的集合类:静态的集合类的生命周期和应用程序的生命周期一样长;
单例模式可能会造成内存泄露:实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收;
数据库、网络、输入输出流,这些资源没有显示的关闭
使用非静态内部类:非静态内部类对象的构建依赖于其外部类
反射
在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用他的任一方法,这种动态调用对象方法以及动态调用获取信息的功能成为Java反射机制。
提供的功能:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法
原理,获取类的Class对象,然后反向获取类或者对象的各种信息
反射的应用场景:
我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性。
通过反射创建对象
方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance("Hello");
面向对象特性
抽象,封装,继承,多态
1.抽象就是把现实生活中的某一类东西提取出来,用程序代码表示,我们通常叫做类或者接口。抽象包括两个方面:一个是数据抽象,一个是过程抽象。数据抽象也就是对象的属性。过程抽象是对象的行为特征。
2.封装把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行封装隐藏。封装分为属性的封装和方法的封装。核心思想就是“隐藏细节”、“数据安全”:将对象不需要让外界访问的成员变量和方法私有化,只提供符合开发者意愿的公有方法来访问这些数据和逻辑,保证了数据的安全和程序的稳定。好处:良好的封装能减少耦合,对成员变量更精确的控制。
3.继承子类可以继承父类的非私有方法和属性(默认属性和方法也不行),达到复用代码的效果。Java是单继承,一个类只能继承一个父类。
4.多态不同类的对象对同一消息作出不同的响应叫做多态。同一消息可以根据发送对象的不同而采用多种不同的行为方式。可以用于消除类型之间的耦合关系,
Spring 的核心就是多态和面向接口编程。多态的分类,编译时多态,方法的重载,运行时多态,方法的覆盖。多态存在的条件:存在继承关系,子类重写父类的方法,父类引用指向子类。
重载和重写
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类;如果父类方法访问修饰符为private则子类中就不是重写
抽象类
-
用abstract关键字修饰一个类,叫抽象类
-
用abstract关键字修饰一个方法,叫抽象方法
-
抽象类作用于设计上,让子类继承并实现。
-
框架和设计模式比较多
细节
-
抽象类不能被实例化
-
抽象类不一定有sbstract方法
-
类包含abstract方法则类的声明就是abstract类
-
abstract只修饰方法
-
抽象类还是类(有非抽象方法,构造器等)
-
不能有主体,所有不能实现
-
若继承抽象类,必须实现所有抽象方法,除非声明类为abstract类
-
抽象方法不能使用private,final和static修饰,这些关键字和重写方式相违背
接口
接口没有实现方法封装
细节
-
接口不能被实例化
-
接口中所有方法为public方法,抽象方法可以不用abstract修饰
-
一个普通类实现接口必须将接口中所有方法实现
-
抽象类实现接口可以不用实现接口中的所有方法
-
一个类可以实现多个接口
-
接口属性只能是final且是,public static final
-
接口中的属性访问形式:接口名.属性名
-
一个接口不能继承其他类,但是可以继承多个接口
-
接口修饰符只能是public和默认与修饰符一样
接口多态
-
多态参数
-
多态数组,调用,向上转型,向下转型
-
接口类型可以指向实现该接口类的对象实例,接口多态传递
抽象类和接口
抽象类: 抽象类可以有构造函数,可以有普通成员变量,方法可以被static修饰,可以有抽象方法和普通方法,一个类只能继承一个抽象类,抽象类主要用于基础类使用,想拥有一些方法但方法没有被实现。
接口:接口不可以有构造函数,只能有常量,方法不可以被static修饰,接口方法全是抽象方法,一个类可以继承多个接口,接口主要用于模块与模块之间的调用,主要用接口实现多继承
1.如果要实现的类和该抽象类是同一类事物,则用继承抽象类的方法;不是同一类事物,就用接口;
2.要设计较小的功能模块,用接口,要设计较大的功能单元,用抽象类;
3.如果大部分方法都不确定,用接口抽象所有方法;如果只有少部分方法不确定,另有部分方法是确定的,用抽象类,实现部分确定的方法,抽象部分不确定的方法。
设计层次:
抽象类是对类抽象,是对整个类整体进行抽象包括属性,行为,抽象类是自底向上抽象而来
接口是对行为的抽象,是对局部(行为)进行抽象,接口是自顶向下设计出来。
内部类
一个类内部嵌套了另一个类结构,类的五大成员,属性,方法,构造器,代码块,内部类
-
定义在外部的局部位置。局部内部类,匿名内部类
-
定义在外部成员位置。成员内部类,静态内部类
局部内部类
-
可以访问外部类的所有成员包括私有
-
不能添加访问符,可以用final修饰,局部变量也可以使用final
-
作用域,仅定义它的方法/代码块
-
局部内部类->外部类成员,直接访问
-
外部类成员->局部内部成员,创建对象访问
-
外部其他类,不能访问局部内部类
-
外部类和局部内部类重名,遵守就近原则可以使用(外部类名.this.成员)
匿名内部类
-
本质是类,匿名内部类,该类无名,是对象,内部类使用一次之后就没有了,不能再次使用
-
使用匿名内部类也是类也是对象
//方式一 new A(){ @override System.out.println(""); }.cry(); //方式二 A a = new A(){ public void cry(){ System.out.println(""); } }; a.cry();
-
可以直接访问外部类所有成员,包含私有
-
不能添加修饰符,地位是局部变量
-
作用域,仅定义它的方法/代码块
-
匿名内部类访问外部成员直接访问
-
外部其它类不能访问匿名内部类
-
内部和外部类重名,内部访问遵守就近原则,访问外部(类.this.成员)
成员内部类
定义在外部类的成员位置无static修饰
-
可以访问外部类的所有属性
-
可以添加访问修饰符(四种)他就是一个成员
-
作用域和外部成员一样
-
成员内部类访问外部类直接访问
-
外部类访问成员内部类创建队形访问
-
外部其它类访问成员内部类
-
外部类和其他内部成员重名,默认遵守就近原则访问外部成员类(外部类名.this.成员)
静态内部类
定义在外部类的成员位置有static修饰
-
可以访问静态成员,不能直接访问非静态成员
-
可以添加访问修饰符(四种)
-
作用域同外部成员为整体
-
静态内部类访问外部类
-
外部类访问静态内部类创建对象访问
-
外部其他访问静态内部类
-
重名时默认遵守就近原则
static关键字和final关键字
static关键字
static的主要作用在于创建独立于具体对象的域变量或者方法。
类变量随着类的加载而存在于方法区中。实例变量随着对象的建立而存在于堆内存中。类变量生命周期最长,随着类的消失而消失。实例变量生命周期随着对象的消失而消失。
static修饰方法:静态方法。不依赖于对象就可以访问,可以直接类名.静态方法访问。静态方法不可以访问对象的非静态方法和非静态变量。
static变量:静态变量被所有的对象所共享,在内存中只有一个副本,存在方法区中,当且仅当在类初次加载时会被初始化。
static静态代码块:在类被初次加载时执行,且执行一次,通常将只需要进行一次的初始化操作都放在static代码块中进行。
static静态内部类:内部类:定义在类内部的类叫做内部类,内部类持有外部类的引用,所以能够访问外部类的所有变量和方法,内部类一般只为外部类使用,且内部类能够独立的继承接口。外部类对象通过‘外部类名.this.xxx’的形式访问内部类的属性与方法。static修饰之后就叫做静态内部类,或嵌套类 [1]要创建静态内部类的对象,并不需要其外部类的对象;也没有持有外部类的引用。 [2]不能够从静态内部类的对象中访问外部类的非静态成员。
final关键字
final修饰变量表示常量,只能被赋值一次,赋值后值不再改变.这里的不可变,是指引用不可变,如果修饰list,随便增删。如果修饰的类的成员变量,必须显示初始化,要么直接赋值,要么构造方法中初始化。否则就会报编译错误
final修饰方法,表示方法不可被重写。final方法编译的时候静态绑定,所以比非final方法快。private方法其实也是final的。final方法在编译阶段绑定,称为静态绑定(static binding)。
final修饰类,表示不可被继承。final类中的所有方法都是final方法。
不用final还可以用什么办法使得这个类不被继承、
将我们的类的构造器声明为private类型的。然后继承的子类,必须得调用父类的构造方法, 因为他是私有的构造函数,不能调用,出错。
final,finally,finalize关键字
Final
-
final用于修饰类、成员变量和成员方法。
-
final修饰的类,不能被继承(String、StringBuilder、StringBuffer、Math,不可变类),其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用);
-
final修饰的方法不能被重写,但是子类可以用父类中final修饰的方法;
-
final修饰的成员变量是不可变的,如果成员变量是基本数据类型,初始化之后成员变量的值不能被改变,如果成员变量是引用类型,那么它只能指向初始化时指向的那个对象,不能再指向别的对象,但是对象当中的内容是允许改变的。
-
final关键字的好处:
final方法比非final快一些
final关键字提高了性能。JVM和Java应用都会缓存final变量。
final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
使用final关键字,JVM会对方法、变量及类进行优化。
Finally:
通常和try catch搭配使用,保证不管有没有发生异常,资源都能够被释放(释放连接、关闭IO流)。当try中有return时执行顺序:return语句并不是函数的最终出口,如果有finally语句,这在return之后还会执行finally(return的值会暂存在栈里面,等待finally执行后再返回)
Finalize
Finalize是object类中的一个方法,子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显示的调用,在垃圾回收时垃圾回收器会主动调用。
集合
集合是可以动态保存任意多个元素,有一系列的操作对象的方法,使用集合增删改查方便,Java集合类里面最基本的接口有:Set:不包含重复元素的Collection。 List:有顺序的collection,并且可以包含重复元素。 Map:可以把键(key)映射到值(value)的对象,键不能重复。
单列 | ArrayList | ||
---|---|---|---|
collection接口 | list | LinkedList | |
Vector | |||
set | HashSet | ||
TreeSet | |||
双列 | map接口 | HashMap | LinkHashMap |
TreeMap | |||
HashTable | properties |
Collection接口可以存放多个元素,有些可以重复有些不能,有些有序List有序,有些无序set。Collection可以通过子接口set/List实现
Collection方法,add添加单个元素,remove删除指定元素,contains查找元素是否存在,size获取元素个数,isEmpty获取元素是否为空,clear清空,addAll添加多个元素,containAll查找多个元素是否存在,removeAll删除多个文件
Collection遍历
-
迭代器
-
Lteractor对象称为迭代器,用于遍历Collection
-
每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作.
-
Lterator仅用于集合遍历,本身并不存放1对象
-
-
增强for循环
增强for循环可以代替itatory,只能用于遍历集合数组
list接口
ArrayList注意事项
-
permit all elements,include null arraylist 可以加入null并且多个
-
ArrayList是用数组完成数据存储
-
ArrayList等同于Vector除了ArrayList是不安全的,执行效率高,多线程不建议使用
ArrayList原理底层
ArrayList底层维护了一个Object数组 elementData.[debuge].trainsient Object[] element.Data
创建一个ArrayList对象,使用无参构造,初始elementData容量为0,第一次添加扩容10,在每次扩容elementData扩容按1.5倍扩容
使用指定大小则初始容量指定大小如需要扩容,则直接扩容elementData为1.5倍扩,扩容使用的事Arrays.copyOf()
Vector底层
Vector底层是对象数组,Protected Object[] elementData
Vector底层是线程同步的方法带有synchronized关键字
需要线程同步安全使用Vector
第一次默认扩容为10,需要扩容时按两倍扩容,指定大小同理
LinkedList原理底层
LinkedList底层实现双向链表和双端队列
可以有重复元素,可重复,可以为null
线程不安全,LinkedList中维护了两个属性first/last首尾节点,维护prev,next,item三属性在每个节点
LinkedList添加删除不是通过数组实现的所以效率高
set接口
HashSet原理底层
-
HashSet实现了set接口
-
HashSet底层实际是HashMap
-
可以存放null但是只能存放一个null
-
HashSet不能保证有序,取决于Hash后在确定索引结果,不保证存入和取出顺序一致
-
不能有重复元素/对象
(hash() + equals())
添加一个元素,先得到hash值会转为索引值
找到存储表table,先看索引是否为空
如果没有直接加入
如果有,调用equals比较,相同放弃添加,不同则添加在最后
一个链表元素超过TREEIRY_THRESHOLD(默认为8)且table大小 >= MIN_TREEIFTY_CAPACITY(默认为64),则进行树化。
底层
Hashset第一次添加table数组扩容到16,加载因子lordFactor = 0.75
到临界值12,就会扩容到32,新临界值为32 * 0.75 = 24
数组链表一旦达到TREEIRY_THRESHOLD(默认为8)且table大小 >= MIN_TREEIFTY_CAPACITY(默认为64)就会进行树化,否则进行扩容
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就是一个HashMap的数组 if语句表示当前table是null或大于等于0 就是第一次扩容到16 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /* 根据key得到hash去计算key应该存放在table的那个位置并把对象赋值给p 判断p是否为空 若p为空表示没有存放元素 创建一个Node(key = `` valve = PRENT) 在该位置tab[i] = newNode() */ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //开发技巧,需要局部变量时(辅助)需要定义时在定义 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; /* 如果当前索引位置对应链表第一个元素和准备添加的元素hash一样且满足以下两个条件 1.加入key和p指向Node结点相同,同一个对象 2.p指向Node结点的key的equals和准备加入的key相同就不能加入 */ else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); /* 判断是不是红黑树如果是用putTreeVal添加 */ else { /* 如果table对应的索引是链表for循环 1.依次和该链表的每一个元素进行比较后都不相同,则添加到链表的最后 ,添加得到链表后判断是不是结点已经到8,调用treeifyBin(tab, hash) 对链表进行树化(转成红黑树) 进行树化时要判断 if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) 上面条件成立进行table扩容,条件不成立进行树化 依次和每一个元素比较有相同的直接break */ 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; } if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } 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; }
LinkedHashSet原理底层
LinkedHashSet是HashSet的子类
LinkedHashSet底层LinkedHashSet维护了一个数组加双向链表
LinkedHashSet根据元素HashCode值决定元素的存储位置,同时用链表维护了次序,使得看起来是按顺序存取
不允许添加重复元素
-
LinkedHashSet维护了一个hash表和双向链表(head 和 tail)
-
每个节点都有pre和next属性,可以形成双向链表
-
添加元素先求Hash值在求索引,确定HashTable位置将元素添加到双向链表(元素重复则不添加) tail.next = new element,new Element.pre = tail,tail = new Element
-
遍历LinkedHashSet确保插入和遍历顺序一致
可以重写equals和HashCode方法让标识符存入一个地址
第一次添加时,数组扩容到16,存放节点类型是LinkedHashMap$Entry
数组是LinkedHashMap$Node,存放元素是LinkedHashMap$Entry类型
Map接口
-
Map与Collection并列存在,用于保存有映射关系的数据Key-Value
-
Map中的Key和value是任何引用类型的数据会封装到HashMap$Node对象中
-
Map中的Key不允许重复
-
Map中的Value可以重复
-
Map中的Key和Value可以为Null,Key为Null只有一个Value可以为多个
-
常用Sting类作为Map的Key
-
Key和Value存在单向一对一,通过Key可以找到Value,有相同的Key等价与替换
-
Map存放数据Key-value,一对KV是放在一个HashMap$Node的,因为Node实现了Entry接口
-
Key-Value最后是HashMap$Node node = new Node(hash key, value null)
-
K-V方便程序员遍历,创建EntrySet集合,存放元素类型Entry,而Entry对象有k,v 。EntrySet Entry<k,v>即trainsient set<Map.Entry<k,v>>entryset;
-
EntrySet中定义的是Map.Entry实际上存的还是HashMap$Node因为,Static Class Node<K,V> implements Map.Entry<k,v>
-
把HashMap$Node对象存放到EntrySet方便我们遍历因为,Map.Entry有重要的方法k.getKey();v.getValue()
-
ConcurrentHashMap原理
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。
HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。
HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。
Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。
CoucurrentHashMap是线程安全的,采用分段锁机制,减少锁的粒度。
TreeMap的底层实现
TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。
红黑树的插入、删除、遍历时间复杂度都为O(lgN),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键的值的大小有序输出。红黑树性质:
性质1:每个节点要么是红色,要么是黑色。
性质2:根节点永远是黑色的。
性质3:所有的叶节点都是空节点(即 null),并且是黑色的。
性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
排序
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|---|---|
直接插入排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 稳定 | 简单 |
希尔排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 不稳定 | 较复杂 |
直接选择排序 | O(n2)O(n2) | O(n2)O(n2) | O(n2)O(n2) | O(1)O(1) | 不稳定 | 简单 |
堆排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(1)O(1) | 不稳定 | 较复杂 |
冒泡排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 稳定 | 简单 |
快速排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | 不稳定 | 较复杂 |
归并排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(n)O(n) | 稳定 | 较复杂 |
基数排序 | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(n+r)O(n+r) | 稳定 | 较复杂 |
直接插入排序
直接插入排序的基本操作是将一个记录插入到已经排好的有序表中,从而得到一个新的、记录数增1的有序表。对于给定的一组记录,初始时假定第一个记录自成一个有序序列,其余记录为无序序列。接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直到最后一个记录插到有序序列中为止
public class InsertSort { public static void insertSort(int[] a) { int i, j, insertNote;// 要插入的数据 for (i = 1; i < a.length; i++) {// 从数组的第二个元素开始循环将数组中的元素插入 insertNote = a[i];// 设置数组中的第2个元素为第一次循环要插入的数据 j = i - 1; while (j >= 0 && insertNote < a[j]) { a[j + 1] = a[j];// 如果要插入的元素小于第j个元素,就将第j个元素向后移动 j--; } a[j + 1] = insertNote;// 直到要插入的元素不小于第j个元素,将insertNote插入到数组中 } } public static void main(String[] args) { int a[] = { 38,65,97,76,13,27,49 }; insertSort(a); System.out.println(Arrays.toString(a)); } }
希尔排序
希尔排序算法是插入排序的一种更高效的改进版本。它的作法不是每次一个元素挨一个元素的比较。而是初期选用大跨步(增量较大)间隔比较,使记录跳跃式接近它的排序位置;然后增量缩小;最后增量为 1 ,这样记录移动次数大大减少,提高了排序效率。希尔排序对增量序列的选择没有严格规定。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
static void shellSort(int[] source){ int group = source.length/2; while (group > 0) { for(int i=group; i<source.length; i++){ if(source[i-group] > source[i]){ int insertData = source[i]; int j=i; while (j > group-1 && source[j-group] > insertData) { source[j]=source[j-group]; j-=group; } source[j]=insertData; } } group = group/2; } }
直接选择排序
直接选择排序又称简单选择排序,是一种不稳定的排序方法,其是选择排序中最简单一种,其基本思想是:第 i 趟排序再待排序序列 a[i]~a[n] 中选取关键码最小的记录,并和第 i 个记录交换作为有序序列的第 i 个记录。
其实现利用双重循环,外层 i 控制当前序列最小值存放的数组元素位置,内层循环 j 控制从 i+1 到 n 序列中选择最小的元素所在位置 k
void selectSort(int a[],int n){ for(int i=1;i<=n-1;i++){//进行n-1趟选择 int index=i; for(int j=i+1;j<=n;j++)//从无序区选取最小的记录 if(a[index]>a[j]) index=j; if(index!=i) swap(a[i],a[index]);
堆排序
冒泡排序
快速排序
归并排序
基数排序
异常体系
异常也是一种对象,java当中定义了许多异常类,并且定义了基类java.lang.Throwable作为所有异常的超类。Java语言设计者将异常划分为两类:Error和Exception。
1、Error(错误):是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时JVM出现问题。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。StackOverflowError,OutOfMemoryError。
2、Exception(异常):程序本身可以捕获并且可以处理的异常。
Exception这种异常又分为两类:运行时异常和编译异常。
1、运行时异常(免检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。ArithmeticException,ClassCastException,NumberFormatException,IllegalArgumentException。
2、编译异常(受检异常):Exception中除RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理,否则编译不通过。在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。ClassNotFoundException,InterruptedException。
int和Integer有什么区别
为了编程方法, Java有八大基本数据类型。又为了能将这些基本数据类型当做对象处理,又为每个基本数据类型引入了对应的包装类型。JDK1.5之后,引入自动拆箱机制,基本数据类型和包装类可以互换。
Integer是int的包装类,必须实例化为才能使用,Integer变量实际是对象的引用,指向对象,默认值为null;
int是基本数据类型,不必实例化就能用,它直接存数据值,默认是0.
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true。因为自动拆箱。
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false。
自动装箱是Java编译器在基本数据类型和对应的对象包装类型之间做的一个转化。比如:把int转化成Integer,double转化成Double,等等。反之就是自动拆箱。
JDK、JRE、JVM的区别
JDK是 Java 语言的软件开发工具包(SDK)。在JDK的安装目录下有一个jre目录
jre是java运行时环境,有两个文件夹bin和lib,可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib合起来就称为jre。
利用
JDK(调用JAVA API)开发了JAVA程序后,通过JDK中的编译程序(javac.exe)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。
泛型
泛型是参数化类型,创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
类型擦除:泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。
过程:
1)将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
2)移除所有的类型参数。
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类
-
泛化代码,代码可以更多的重复利用。不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据;
-
消除强制类型转换。只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。
-
类型安全,提供编译期间的类型检测。
-
性能较高,用GJ(泛型JAVA)编写的代码可以为java编译器和虚拟机带来更多的类型信息,这些信息对java程序做进一步优化提供条件。
泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,在运行时不存在任何类型相关的信息。例如 List<String>在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型
类型擦除所有类型参数都用他们的限定类型替换:
比如T->Object ? extends BaseClass->BaseClass
什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。有两种限定通配符:
一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,
另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。
泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>
表示了非限定通配符,因为<?>可以用任意类型来替代。
Object类的方法
1、Object构造方法
2、clone()
返回的对象为浅拷贝,clone()方法同样是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法,具体的实现是有C/C++完成的。clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
相关问题:
(1)new 和 clone的区别:
clone()不会调用构造方法;new会调用构造方法
clone()更快。clone()能快速创建一个已有对象的副本,即创建对象并且将已有对象中所有属性值克隆;new只能在JVM中申请一个空的内存区域,对象的属性值要通过构造方法赋值
(2)深拷贝和浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
实现对象拷贝的类,必须实现Cloneable接口,并覆写clone()方法。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝比浅拷贝速度慢且花销较大。
若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
3、getClass()
getClass()也是一个native方法,返回的是此Object对象的类对象/运行时类对象Class<?>。效果与Object.class相同。
4、equals()
==表示的是变量值相同(对于基本数据类型,地址中存储的是值,引用类型则存储指向实际对象的地址);比地址
equals是对象的内容是否相同,重写,则按重写比,否则按object的。比值
重写equals()方法必须重写hasCode()方法
5、hashcode()
hashCode()方法返回一个整形数值,表示该对象的哈希码值。
如果两个对象相等(依据:调用equals()方法),那么这两个对象调用hashCode()返回的哈希码也必须相等;
判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将Set进行一次遍历,并逐一用equals()方法判断两个对象是否相等,此种算法时间复杂度为o(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可。result*31 = (result<<5) - result
hashCode()相同的两个对象,不一定相等,换言之,不相等的两个对象,hashCode()返回的哈希码可能相同。
6、toString()
toString()方法返回该对象的字符串表示。
7、wait()
wait()使当前线程阻塞,前提是 必须先获得锁,配合synchronized 关键字使用。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
8、notify() / notifyAll()
notify()/notifyAll()方法调用后,其所在线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,此时释放锁,因此,如果其同步代码块后还有代码,其执行则依赖于JVM的线程调度。
JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set,也有人翻译为锁池和等待池
对于Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。
对于Wait Set:如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。
某个线程B想要获得对象锁,一般情况下有两个先决条件
一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等)
二是线程B已处于RUNNABLE状态。
对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。
对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转到Entry Set中。
然后,每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。
尽量使用notifyAll()的原因就是,notify()非常容易导致死锁,但是notifyall唤醒全部也会造成开销。
9、finalize()
finalize方法主要与Java垃圾回收机制有关。二次标记。
string
-
保存字符串
-
对象“”号字符序列
-
字符使用Unicode编码
-
String常用构造器(使用手册)
String S1 = new String()
String S2 = new String(String original)
String S3 = new String(char[] a)
String S4 = new String(char[] a,int starIndex,int count)
String S5 = new(byte Ib)
-
string实现接口Serializable可以实现串行化,可以在网络中进行传输,接口Comparable,可以比较大小
-
String是final类不能被继承
-
String有属性private final char value[]用于存放字符串内容
-
value是一个final类型,不可修改,指向地址不能被修改,即value不能指向新地址,字符串单个字符可以修改
创建String对象的两种方式
1.使用赋值 String s = "hapedu"
2.调用构造器 String S2 = new String("hsp")
(1)查看是否有指向“hsp”的空间,有则直接指向,没有则重新创建,s指向最终的常量地址
(2)先在堆中创建空间维护 value 指向 “hsp”空间,没有则重新创建,有则通过 value 指向,指向堆中的空间
垃圾回收
有三种,都是基于可达性分析算法,
有复制算法(把存货对象复制到另外一块儿区域空间利用率只有一半适合适合新生代对象无法创建时触发),
标记整理算法(适合老年代,对存活的对象进行移动,从做后一个对象的地址开始删除后续地址,可以再次申请,缺点是要移动大量的对象,优点是没有内存碎片),
清除算法(只清除死亡的对象)
GC有七个垃圾回收器
Serial,ParNew,Parallel,parallelOld,CMS,SerialOld,G1
StringBuffer
很多方法与String相同,但StringBuffer是可变长度的,且是容器,可以对字符串进行增删改查
-
StringBuffer直接从父类AbstractStringBuffer
-
StringBuffer实现了Serializable,StringBuffer对象可以串行化,可以在网络中进行传输
-
父类AbstractStringBuffer有属性char[],value,不是final类,该value数组可存放字符串内容
-
StringBuffer是一个final类不能被继承
StringBuffer的构造器
-
StringBuffer()创建一个大小为16的char[],用于存放字符
-
StringBuffer(int capacity)创建一个自定大小的的char[]大小为capacity存放字符
-
StringBuffer(String str)创建一个有指定字符串的地址的字符空间
StringBuilder
可变字符序列,与StringBuffer兼容API该类设计为StringBuffer的简单替换在字符缓冲区,单线程时使用,比StringBuffer快
StringBuilder主操作append 和 insert可重载接受所有数据类型
-
StringBuilder直接从父类AbstractStringBuffer
-
StringBuilder实现了Serializable,StringBuffer对象可以串行化,可以在网络中进行传输
-
父类AbstractStringBuffer有属性char[],value,不是final类,该value数组可存放字符串内容
-
StringBuilder是一个final类不能被继承
-
StringBuilder 没有做互斥处理,没有synchronize关键字,因此单线程使用
String,StringBuilder,StringBuilder 比较
-
StringBuffer与StringBuilder方法一样
-
String是不可变字符序列,效率低下,重复率高
-
StringBuffer可变字符串序列,效率高,线程安全
-
StringBuilder可变字符串,效率最高,线程不安全单线程
-
String对错执行,改变字符串操作,会改写大量副本存入内存,极大影响性能
结论:
字符有大量修改用StringBuilder或者StringBuffer。 单线程使用StringBuilder,多线程使用StringBuffer,很少修改,多对象引用,String
线程
有三种方式可以用来创建线程:继承Thread类,实现Runnable接口,应用程序可以使用Executor框架来创建线程池
启动线程的三种方法
一、继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程
二、通过Runnable接口创建线程类
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
线程池(thread pool)
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。
所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
-
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
-
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
-
newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
-
newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
-
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池有哪些类型?如果线程池满了再往里面塞任务会向哪个位置存放?有哪些策略?
-
线程池原理:任务到来时,先判断核心线程数是否已满,如果没满则执行,如果已满则判断任务队列是否已满,若没满则存放在任务队列,若已满则比较线程池最大容量,如果没满则创建线程继续执行,如果已满就判断
-
newCachedThreadPool适合执行大量的耗时短的任务,
newFixedThreadPOOL定长线程池只有核心线程并且不会被回收,
newSchedulexdThreadPool定长线程池,核心线程数固定,非核心线程在空闲时会回收,newSIngleThreadExecutor单线程化的线程池使用无界队列让多个任务在同一个线程中有序执行
-
如果满了的话会将池队列(阻塞队列),有四种策略
AbortPolicy丢弃任务,直接抛出异常
CallerRunsPolicy直接执行
DiscardPolicy忽视任务
DiscardOldestPolicy丢弃队列里面最近的任务然后执行这个任务
线程池优点
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程同步和线程调度
-
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
-
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
-
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
-
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);
在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
线程从创建到死亡的几种状态
-
新建( new ):新创建了一个线程对象。
-
可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。
-
运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
-
阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种: (一). 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。 (二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。 (三). 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
-
死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
为什么使用synchronized而不使用ReentrantLock?
可重入锁:判断所有没有被锁上,再判断是谁锁上的,若是自己锁上的可以再次访问临界资源并继续加锁,加锁和解锁次数一样
不可重入锁:判断锁有没有被锁上
synchronized:关键字,由jvm支持,A获取锁后B会一直等待直到释放锁,竞争不激烈其,释放锁有两种途径:获取锁的线程执行完毕或执行代码出现异常
reentantLock:可重入锁,其是类,竞争激烈,有三种获取锁的方式:lock(),lockinterruptibly()可中断,tryLock()尝试获取锁,在一定等待时间内如果没有获取到则中断 ,通过unlock()释放锁
使用synchronized而不使用ReentrantLock原因:1.减少内存开销(后者节点通过继承AQS来获取同步支持)2.前者内部优化,能在运行时做出优化措施(锁粗化,锁自旋,锁消除)
IO
文件创建CreatNewFile方法是真正创建文件的
new file(String Dathname); //根据路径创建 new File(File Parent,String Child); //根据父级目录文件 + 子路径创建 new File (String Parent,String Child); //根据父级目录 + 子路径创建
常用文件操作
getname()获取文件名, getAbsolutePath() 文件绝对路径,getPath()文件父级目录,length()文件字节大小,exists() 文件是否存在,isFile()是不是一个文件,isDirectory()是不是一个目录,
mkdir创建一级目录,mkdirs创建多级目录,delete删除空白目录或文件
IO流原理
-
IO流是Inpute/outpute的缩写,IO是非常实用的方法
-
java程序中,对于数据输入/输出操作以流Stream方式进行
-
java提供了不同的流的接口,以获取不同类型的数据
-
输入inpute读取外部数据,到内存程序中
-
输出outpute将内存中的程序,输出到磁盘,光盘等存储设备中
InputeStream字节输入流
InputeStream是抽象类是所有字节输入流的超类
FileInputStream文件输入流
public class FileCreate { public static void main(String[] args) { } @Test public void readFile01(){ String filePath = "g:\\hello.text"; int readLine = 0; byte[] buf = new byte[8]; FileInputStream fileInputStream = null; try { //创建fileInputStream对象用于读取文件 fileInputStream = new FileInputStream(filePath); /** * 从该输入流读取最多b.length字节数据到字节数组,此方法阻塞,直到输入 * 如果返回-1,表示读取完毕 * 如果读取正常,返回实际读取字节数组 */ while ((readLine = fileInputStream.read(buf)) != -1){ System.out.println((char)readLine); } } catch (IOException e) { e.printStackTrace(); }finally { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
BufferedInputStream缓冲字节输入流
public class BufferReader01_ { public static void main(String[] args) throws Exception { String filePath = "g:\\hello.txt"; //创建BufferdeReader BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath)); //读取 String line; //bufferedReader.readLine()是按行读取 //当返回null时,表示文件读取完毕 while ((line = bufferedReader.readLine()) != null){ System.out.println(line); } //关闭流,只需要关闭BufferedReader,因为底层会去自动关闭节点流 bufferedReader.close(); }
ObjectInputStream对象字节输入流
OutputStream字节输出流
FileOutputStream
@Test public void writeFile(){ //创建FileOutputStream对象 String filePath = "g:\\hello.text"; FileOutputStream fileOutputStream = null; try { //得到一个对象fileOutputStream // new FileOutputStream(filePath)创建方式是当写入内容是,会覆盖原来的内容 //new FileOutputStream(filePath,true)创建方式是,当写入内容时,追加到文件后面 fileOutputStream = new FileOutputStream(filePath,true); //写入一个字符串 String str = "hello world"; //str.getBytes()可以吧字符串转成字节数组 fileOutputStream.write(str.getBytes()); //write(byte [] b,int off,int len),len将字节从位于偏移量,off的指定字节数组写入此文件输出流 fileOutputStream.write(str.getBytes(),0,3); } catch (IOException e) { e.printStackTrace(); }finally { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
BufferedInputStream
拷贝文件图片
@Test public void readFile02(){ String scrFilePath = "g:\\jsp.jpg"; String destFilePath = "e:\\jsp.jpg"; FileOutputStream fileOutputStream = null; FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(scrFilePath); fileOutputStream = new FileOutputStream(destFilePath); //定义一个数组提高读取效率 byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = fileInputStream.read(buf)) != -1){ //读收到后就写入文件通过fileOutputStream //级一边读一边写 fileOutputStream.write(buf,0,readLen); } System.out.println("success for copy"); } catch (IOException e) { e.printStackTrace(); }finally { try { if (fileInputStream != null) fileInputStream.close(); if(fileOutputStream != null) fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }
FileRead/FileWriter
FileRead
public class FileRead01_ { public static void main(String[] args) { String filePath = "g:\\hello.txt"; FileReader fileReader = null; int readLen = 0; char[] buf = new char[8]; try { fileReader = new FileReader(filePath); while (( readLen = fileReader.read(buf)) != -1){ System.out.print(new String(buf,0,readLen)); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileReader != null) fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
FileWriter
@Test public void FileWriter01(){ String filePath = "g:\\hello.txt"; FileWriter fileWriter = null; char[] chars = {'a','b','c'}; try { fileWriter = new FileWriter(filePath,true); // write(int ) 写入单个字符 fileWriter.write('h'); // write( char[])写入指定数组 fileWriter.write(chars); //write( char[],off Len)写入指定数组的指定部分 fileWriter.write("大家都来多看看".toCharArray(),0,5); // write( stream)写入整个字符串 fileWriter.write("十多个购物券股份股东会"); //write( stream ,off ,len)写入字符串的指定位置 fileWriter.write("到时干啥的根深蒂固",0,2); } catch (IOException e) { e.printStackTrace(); } finally { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
序列化和反序列化
java序列化是指把java对象转换为字节序列的过程,而java反序列化是指把字节序列恢复为java对象的过程。
序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。
只有实现了Serializable或Externalizable接口的对象才能被序列化,否则抛出异常!
Socket通信
-
套接字(Socket)开发网络应用程序广泛采用,成为事实的标准
-
通信两端都有Socket。是两台机器间通信的端点
-
网络通信其实就是Socket间通讯
-
Socket允许程序把网络连接当成一个流,数据在Socket两端通过IO传输
-
一般主动发起通讯的应用程序属于客户端,等待通讯请求为服务端
当我们需要通讯时(读写数据)1.Socket.OutPutStream()2,socket.geInputStream()
当我们需要通讯时(读写数据)1,socket.getOutputSream()2,socket.getInputeStream()
设置写入结束标记socket.shutdownOutput();
package com.Socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketTCP01Service { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9999); System.out.println("服务端999正在被占用"); //当客户端连接9999端口实际程序会进行阻塞等待连接如果有连接则会返回Socket对象程序继续 Socket socket = serverSocket.accept(); System.out.println("服务端socket = " + socket.getClass()); //通过socket.getInputStream();读取客户端写入到数据通道中显示 InputStream inputStream = socket.getInputStream(); //IO读取 byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1) { System.out.println(new String(buf,0,readLen)); //根据读取到的实际长度显示内容 } //获取socket县关联的输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("hello,client".getBytes()); //结束标记 socket.shutdownOutput(); //关闭流和Socket outputStream.close(); inputStream.close(); serverSocket.close(); socket.close(); System.out.println("客户端退出"); } }
package com.Socket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; public class SocketTCP01Client { public static void main(String[] args) throws IOException { //连接服务器(ip ,端口),连接成功返回Socket对象 Socket socket = new Socket(InetAddress.getLocalHost(), 9999); System.out.println("客户端返回socket =" + socket.getClass()); /** * 连接上后生成Socket通过socket.getOutputStream() */ OutputStream outputStream = socket.getOutputStream(); /*通过输出流,写入数据到数据通道*/ outputStream.write("hello ,sever".getBytes()); //结束标记 socket.shutdownOutput(); //获取和socket先关联的数据 InputStream inputStream = socket.getInputStream(); byte[] buf = new byte[1024]; int readLen = 0; while ((readLen = inputStream.read(buf)) != -1){ System.out.println(new String(buf,0,readLen)); } //关闭流对象和socket,必须关闭 inputStream.close(); outputStream.close(); socket.close(); System.out.println("客户端退出"); } }
TCP
public class StreamUtils { /*** * 将文件输入流转成byte[]数组,吧文件读取到byte[] */ public static byte[] streamToByteArray(InputStream is) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int len; while ((len = is.read(b)) != -1){ bos.write(b,0,len); } byte[] array = bos.toByteArray(); bos.close(); return array; } /*** * 将InputStream转成Stream */ public static String streamToString(InputStream is) throws Exception{ BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringBuilder builder = new StringBuilder(); String line; while ((line = reader.readLine()) != null){ builder.append(line + "\r\n"); } return builder.toString(); } } ----------------------------------------------------------------------; //客户端 public class TCPFileUploadClient { public static void main(String[] args) throws Exception { //创建客户端连接服务端得到socket对象 Socket socket = new Socket(InetAddress.getLocalHost(), 8888); //创建读取磁盘文件的输入流 String filePath = "g:\\jsp.jpg"; BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath)); //byte就是filePath对应的字节数组 byte[] bytes = StreamUtils.streamToByteArray(bis); //通过Socket获取到输出流,将byte发送给服务端 BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream()); bos.write(bytes);//可将文件中字节数组写入数据通道 bis.close(); socket.shutdownOutput();//结束标记 //接受从服务端回复的消息 InputStream inputStream = socket.getInputStream(); //使用streamToString方法将inputStream 读取的内容转成字符串 String s = StreamUtils.streamToString(inputStream); System.out.println(s); //关闭相关流 bos.close(); socket.close(); System.out.println("结束"); } } ----------------------------------------------------------------------; //程序端 public class TCPFileUploadService { public static void main(String[] args) throws Exception { //服务端代码在本机监听8888端口 ServerSocket serverSocket = new ServerSocket(8888); System.out.println("服务端代码在8888端口监听"); //等待客户端连接 Socket socket = serverSocket.accept(); //读取客户端发送的数据,Socket得到输入流 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); byte[] bytes = StreamUtils.streamToByteArray(bis); //将指定byte数组,写到指定路径获得一个文件 String destFilePath = "src\\qier.png"; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath)); bos.write(bytes); bos.close(); //向客户端回复收到图片 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); writer.write("收到图片"); writer.flush();//将内容刷新到数据通道 socket.shutdownOutput(); //关闭其他资源 bis.close(); socket.close(); serverSocket.close(); System.out.println("结束"); } }
netstat指令
-
netatat-an可以查看本机当前网络情况,包括端口监听和网络连接情况
-
netatat-an | more 可以分页查询
-
要求在dos控制台上执行win + r
说明:
(1)Listening表示端口正在监听
(2)如果有外部程序连接到该端口,就会显示一条连接信息
(3)可以输入ctrl + c退出
TCP网络通讯不为人zhi的秘密
客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通讯的,这个端口是通过TCP/IP来分配的,是随机的
UDP网络通信
-
类DatagramSocket 和DatagramPacket [数据包/数据报]实现了基于UDP协议网络程序
-
UDP数据报通过数据报套接字DatagramSocket发送和接受,系统不保证UDP数据报一定能安全发送到目的地,也不确定什么时候可以抵达
-
DatagramPacket对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接受端的IP地址和端口号
-
UDP协议中每个数据报都给出了完整的地址信息,无需建立发送方和接受方的连接