7.JavaSE: 常用类
7.1 Object类
理论上Object类是所有类的父类,即直接或间接的继承java.lang.Object类,由于所有的类都继承在Object类,因此省略了extends Object关键字
7.1.1 toString()
Object 类的 toString 方法返回一个字符串,该字符串由类名、标记符“@”和此对象哈希码的无符号十六进制表示组成
//源码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public static void main(String[] args){
Object o1 = new Object();
System.out.println(o1.toString());
//java.lang.Object@606d8acf
}
7.1.2 getClass()
返回次Object的运行时类类型,不可重写,要调用的话,一般和getName()联合使用,如getClass().getName()
public final native Class<?> getClass();
public static void main(String[] args) {
Object o = new Object();
System.out.println(o.getClass());
//class java.lang.Object
}
7.1.3 hashCode()
1.返回该对象的哈希码值
2.该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法,这个方法在一些具有哈希功能的Collection中用到
3.一般必须满足obj1.equals(obj2)==true,可以推出obj1.hash Code() == obj2.hashCode(),但是hashCode相等不一定就满足equals,不过为了提高效率,应该尽量使上面两个条件接近等价
public native int hashCode();
public static void main(String[] args) {
Object o1 = new Object();
System.out.println(o1.hashCode());
//1617791695
}
}
7.1.4 notify()
该方法唤醒在该对象上等待的某个线程
public final native void notify();
该方法唤醒在该对象上等待的所有线程
public final native void notifyAll();
7.1.5 finalize()
一、该方法用于释放资源,因为无法确定该方法什么时候被调用,很少使用
protected void finalize() throws Throwable { }
二、工作原理
一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
三、关于垃圾回收,有三点需要记住:
1、对象可能不被垃圾回收,只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放
2、垃圾回收并不等于“析构”
3、垃圾回收只与内存有关,使用垃圾回收的唯一原因是为了回收程序不再使用的内存
四、finalize()的用途
无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对finalize()的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。不过这种情况一般发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式
//不重写则调用Object中默认的finalize方法
public class Action {
public static void main(String[] args) {
Action action = new Action();
action = null;
System.out.println("action对象被回收了");
System.gc();//主动调用垃圾回收器
}
@Override
protected void finalize() throws Throwable {
System.out.println("系统调用了重写后的finalize方法");
}
}
7.1.6 clone()
一、创建并返回此对象的一个副本,两者类型、属性、属性值都相同,只有地址不同。
二、使用步骤:
1.复制对象的所属类先实现Cloneable接口 ,实现该接口意味允许该类进行复制,否则抛出CloneNotSupportedException异常
2.需要在子类中重写clone()方法,然后在重写的clone()方法中通过super.clone()调用继承自Object的clone(),来实现对clone()方法的调用
protected native Object clone() throws CloneNotSupportedException;
//按照惯例,返回的对象应该通过调用super.clone获得,能通过super去调用父类中的方法,说明在子类中是将继承自父类的方法给覆盖了,然后通过super调用,来区分父类的方法和子类重写后的方法
public class Father implements Cloneable{
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void show(){
System.out.println("name:"+name);
}
public Father(String name) {
this.name = name;
}
public Father(){
}
//重写clone方法
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
------------------------------------------------------------------
public class Son{
public static void main(String[] args) throws CloneNotSupportedException {
Father father = new Father("XiaoGuo");
father.show();//XiaoGuo
//复制Father对象father
Father clone = (Father) father.clone();
clone.show();//XiaoGuo
}
}
7.1.7 wait()
一、wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断,wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回
二、调用该方法后线程进入睡眠状态,直到以下事件发生:
1.其他线程调用了该对象的notify方法
2.其他线程调用了该对象的notifyAll方法
3.其他线程调用了interrupt中断该线程
4.时间间隔到了
三、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
7.1.7.1 wait()
导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法
7.1.7.2 wait(long timeout)
long timeout:要等待的最长时间
导致当前线程等待,直到另一个线程调用notify()方法或该对象的notifyAll()方法,或者指定的时间已过
7.1.7.3 wait(long timeout, int nanos)
int nanos:额外时间
导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException
{
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException("nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
7.1.8 equals()
一、Object中的equals方法是直接判断this和obj本身的值是否相等,即用来判断调用equals的对象和形参obj所引用的对象是否是同一对象,所谓同一对象就是指内存中同一块存储单元,如果this和obj指向的同一块内存对象,则返回true,如果this和obj指向的不是同一块内存,则返回false
public boolean equals(Object obj) {
return (this == obj);
}
二、String类已经重写了object中的equals方法,这样就是比较内容是否相等了
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
三、为什么重写equals()方法时,还要必须重写hashCode()方法?
如果使用equals()比较后,返回true的情况下,二者的哈希Code一定相等,如果返回flase的情况下,二者的哈希Code也有可能相等,该过程被我们称为哈希冲突,比如字母a和Integer的97的哈希码是相同的
7.1.9 sleep()和wait()的区别
一、所属类不同
1.sleep()属于线程中的方法
2.wait()属于Object中的方法
Thread.sleep(2000);//线程
----------------------------------------
Object t = new Object();
synchronized{
t.wait();//Object
}
二、方法类型不同
1.sleep()是静态方法,通过类名.调用
2.wait()方法是实例方法,通过创建对象进行调用
三、使用语法不同
1.sleep()可以直接用
2.wait()则需要配合锁一起用
四、唤醒方法不同
1.sleep()中必须设置等待值,超过等待值会自动唤醒
2.在wait()中可以不设置等待值,如果没有等待值wait()则会一直等待
//wait()不传时间的唤醒方式
public static final Object monitor=new Object();
main:
//创建线程
new Thread(()->{
//唤醒锁
synchronized(monitor){
monitor.notify();
//monitor.notifyAll();
}
}).start();
//等待锁
synchronized(monitor){
monitor.wait();
}
五、释放锁资源不同
1.sleep()不会释放资源锁
2.wait()会释放资源锁,并会加到等待队列中
六、线程状态不同
new Thread(()->{
//唤醒锁
synchronized(monitor){
//获取线程状态
System.out.println(main.getState());
}
}).start();
Thread.sleep();//TIME_WAITING
//等待锁
synchronized(monitor){
monitor.wait();//WAITING
}
7.1.10 equals()和==的区别
一、==
/*Java中只有值传递,所以==比较的是值,只不过基本数据类型的值是值本身,而引用数据类型的值,是对象地址*/
1.对于基本数据类型来说比较的为值
int a = 1;
int b = 1;
System.out.println(a==b);//true
2.对于引用数据类型来说比较的是地址,即判断两个引用是否都指向了一个地址
Person p1 = new Person();
Person p2 = p1;
System.out.println(p1==p2);//true
3.所以两个值完全相等的对象,==也会认为不相等
Person p1 = new Person("XiaoGuo");
Person p2 = new Person("XiaoGuo");
System.out.println(p1==p2);//false
二、equals()
/*equals()如果没有重写,则与==等价,而引用类型String中已经将equals()重写,所以比较的是值*/
1.使用equals()进行字符串比较
String s1 = new String("XiaoGuo");
String s2 = new String("XiaoGuo");
System.out.println(s1==s2);//false
System.out.println(s1.equals(s2));//true
2.使用equals()要避免空指针异常
//将不会为null的对象放在前面,可能为null的对象放在后面
//两者均可能为空时,采用object中的equals()方法
String s1 = null;
String s2 = new String("XiaoGuo");
String s3 = null;
System.out.println(s2.equals(s1));//正常
System.out.println(object.equals(s1,s3));//正常
System.out.println(s1.equals(s2));//空指针异常
3.equals()不能用于比较基本数据类型
当比较包装类型时候,要注意比较的值是否和包装类型一致,包装类中重写了equals()方法,它首先会比较二者是否为同一类型,比如包装类为Long,当比较的值为int类型时,int类型自动装箱为Integer类型后与Long类型不是同一类型,此时虽然值可能相等,但是仍返回flase
Long a = 8L;
System.out.println(a.equals(8));//false
System.out.println(a==8);//true
7.1.11 final、finally、finalize的区别
一、final关键字
1.当final修饰类的时候,不允许此类被继承
2.当final修饰方法的时候,不允许任何从此类继承的子类重写此方法
3.当final修饰变量的时候,表示变量一旦初始化便不允许修改
4.当final修饰参数的时候,表示参数在整个方法内不允许修改
二、finally
1.表示一种一定被执行的机制,常见于try-catch-finally或try-finally中,来进行类似关闭JDBC链接、保证释放锁等动作
2.正常情况下finally一定会执行,某种情况下不一定执行
main:
try{
System.out.println("Xiaoguo");
System.exit(0);
}finally{
System.out.println("finally执行了");//XiaoGuo
}
三、finalize
Object中的一个基础方法,在被垃圾收集前完成特定资源的回收,但在JDK9中被标记为弃用,原因是finalize的执行不稳定,无法保证finalize一定会被执行,而且由于其和垃圾收集关联在一起,如果一旦实现了非空的finalize方法,就会导致相应对象回收呈现数量级的变慢,导致其性能也有一定的问题
7.2 Math类
一、Java 的 Math 包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。Math的方法都被定义为 static 形式,通过 Math 类可以在主函数中直接调用
二、常用方法
1.Math.PI: 记录的圆周率
2.Math.E :记录e的常量
3.Math.abs:求绝对值
4.Math.sin :正弦函数 Math.asin 反正弦函数
5.Math.cos :余弦函数 Math.acos 反余弦函数
6.Math.tan :正切函数 Math.atan 反正切函数 Math.atan2 商的反正切函数
7.Math.toDegrees :弧度转化为角度 Math.toRadians 角度 转化为弧度
8.Math.ceil: 得到不小于某数的最大整数
9.Math.flfloor :得到不大于某数的最大整数
10.Math.IEEEremainder :求余
11.Math.max :求两数中最大
12.Math.min :求两数中最小
13.Math.sqrt:求开方
14.Math.pow:求某数的任意次方, 抛出ArithmeticException处理溢出异常
15.Math.exp :求e的任意次方
16.Math.log10 :以10为底的对数
17.Math.log :自然对数
18.Math.rint :求距离某数最近的整数(可能比某数大,也可能比它小)
19.Math.round: 求距离某数最近的整数,返回int型或者long型(上一个函数返回double型)
20.Math.random:返回0,1之间的一个随机数
7.3 Random类
Java中存在着两种Random函数
一、java.lang.Math.Random
调用这个Math.Random()函数能够返回带正号的double值,该值大于等于0.0且小于1.0,即取值范围是[0.0,1.0)的左闭右开区间,返回值是一个伪随机选择的数,在该范围内(近似)均匀分布
public static void main(String[] args) {
// 结果是个double类型的值,区间为[0.0,1.0)
System.out.println("Math.random()=" + Math.random());
int num = (int) (Math.random() * 3);
// 注意不要写成(int)Math.random()*3,这个结果为0或1,因为先执行了强制转换
System.out.println("num=" + num);
}
//结果
//Math.random()=0.44938147153848396
//num=1
二、java.util.Random
1.Random():创建一个新的随机数生成器
public static void main(String[] args) {
Random rand =new Random();
int i=rand.nextInt(100);
//100是随机数的上限,产生的随机数为0-100的整数,不包括100
System.out.println(i);
}
2.Random(long seed):使用单个 long 种子创建一个新的随机数生成器
public static void main(String[] args) {
Random ran1 = new Random(25);
System.out.println("使用种子为25的Random对象生成[0,100)内随机整数序列: ");
for (int i = 0; i < 10; i++) {
System.out.print(ran1.nextInt(100) + " ");
//81 28 47 38 97 98 95 37 96 54
}
}
三、方法摘要
-
protected int next(int bits):生成下一个伪随机数。
-
boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的boolean值。
-
void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。
-
double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布的 double值。
-
float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布flfloat值。
-
double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的double值,其平均值是0.0标准差是1.0。
-
int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。
-
int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在(包括和指定值(不包括)之间均匀分布的int值。
-
long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。
-
void setSeed(long seed):使用单个 long 种子设置此随机数生成器的种子。
7.4 时间日期类
7.4.1 Date类
java.util 包提供了 Date 类来封装当前的日期和时间,Date类提供两个构造函数来实例化 Date对象
一、第一个构造函数使用当前日期和时间来初始化对象
Data();
二、第二个构造函数接收一个参数,该参数是从1970年1月1日起的毫秒数
Date(long millisec)
三、常用方法
1.boolean after(Date date): 若当调用此方法的Date对象在指定日期之后返回true,否则返回false
2.boolean before(Date date):若当调用此方法的Date对象在指定日期之前返回true,否则返回false
3.Object clone( ):返回此对象的副本
4.int compareTo(Date date):比较当调用此方法的Date对象和指定日期,两者相等时候返回0。调用对象在指定日期之前则返回负数,调用对象在指定日期之后 则返回正数
5.int compareTo(Object obj):若obj是Date类型则操作等同于compareTo(Date) 。否则它抛出ClassCastException
6.boolean equals(Object date):当调用此方法的Date对象和指定日期相等时候返回true,否则返回false
7.long getTime( ) :返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数
8.int hashCode( ): 返回此对象的哈希码值
9.void setTime(long time):用自1970年1月1日00:00:00 GMT以后time毫秒数设置时间和日期
10.String toString( ): 把此 Date对象转换为以下形式的 String:dow mon dd hh:mm:ss zzzyyyy,dow是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat)
//获取当前日期
public static void main(String args[]) {
// 初始化 Date 对象
Date date = new Date();
// 使用 toString() 函数显示日期时间
System.out.println(date.toString());
//Sat Apr 23 15:09:43 CST 2023
}
//比较两个日期
//getTime() 获取两个日期(自1970年1月1日经历毫秒数值)
public static void main(String[] args) {
// 初始化 Date 对象
Date date = new Date();
long time = date.getTime();//比较
long time2 = date.getTime();
System.out.println(time==time2);
}
//使用方法before(),after()和equals()
public static void main(String[] args) {
boolean before = new Date(97, 01,05).before(new Date(99, 11, 16));//比较
System.out.println(before);//true
}
7.4.2 SimpleDateFormat类
一、SimpleDateFormat 是一个以语言环境敏感的方式来格式化和分析日期的类。SimpleDateFormat 允许你选择任何用户自定义日期时间格式来运行。例如:
//其中yyyy是完整的公元年,MM是月份,dd是日期,HH:mm:ss是时分秒
public static void main(String args[]) {
Date dNow = new Date( );
SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd hh:mm:ss");
System.out.println("当前时间为: " + ft.format(dNow));
}
二、使用printf格式化日期,printf 方法可以很轻松地格式化时间和日期,使用两个字母格式,它以**%t** 开头并且以下面表格中的一个字母结尾
public static void main(String args[]) {
// 初始化 Date 对象
Date date = new Date();
//c的使用
System.out.printf("全部日期和时间信息:%tc%n",date);
//f的使用
System.out.printf("年-月-日格式:%tF%n",date);
//d的使用
System.out.printf("月/日/年格式:%tD%n",date);
//r的使用
System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);
//t的使用
System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);
//R的使用
System.out.printf("HH:MM格式(24时制):%tR",date);
}
/*结果:
全部日期和时间信息:星期二 四月 24 15:23:45 CST 2023
年-月-日格式:2023-04-24
月/日/年格式:04/24/23
HH:MM:SS PM格式(12时制):03:23:45 下午
HH:MM:SS格式(24时制):15:23:45
HH:MM格式(24时制):15:23 */
三、时间休眠:休眠(sleep)
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用,目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会
public static void main(String args[]) {
try {
System.out.println(new Date( ) + "\n");
Thread.sleep(1000*3); // 休眠3秒
System.out.println(new Date( ) + "\n");
}catch(Exception e){
System.out.println("Got an exception!");
}
}
7.4.3 Calendar类
Calendar类是一个抽象类,在实际使用时实现特定的子类的对象,创建对象的过程对程序员来说是透明的,只需要使用getInstance方法创建即可
//创建一个代表系统当前日期的Calendar对象
public static void main(String args[]) {
Calendar c = Calendar.getInstance();//默认是当前日期
System.out.println(c);
}
//创建一个指定日期的Calendar对象
//创建一个代表2023年4月27日的Calendar对象
Calendar c1 = Calendar.getInstance();
c1.set(2023, 4-1, 27);
//Calendar类对象字段类型
// 获得年份
int year = c1.get(Calendar.YEAR);
// 获得月份
int month = c1.get(Calendar.MONTH) + 1;
// 获得日期
int date = c1.get(Calendar.DATE);
// 获得小时
int hour = c1.get(Calendar.HOUR_OF_DAY);
// 获得分钟
int minute = c1.get(Calendar.MINUTE);
// 获得秒
int second = c1.get(Calendar.SECOND);
// 获得星期几(注意(这个与Date类是不同的):1代表星期日、2代表星期1、3代表星期二,以此类推)
int day = c1.get(Calendar.DAY_OF_WEEK);
//把c1对象的日期加上10,也就是c1也就表示为10天后的日期,其它所有的数值会被重新计算
c1.add(Calendar.DATE, 10);
//把c1对象的日期减去10,也就是c1也就表示为10天前的日期,其它所有的数值会被重新计算
c1.add(Calendar.DATE, -10);
//Calender的月份是从0开始的,但日期和年份是从1开始的
public static void main(String[] args) {
Calendar c1 = Calendar.getInstance();
c1.set(2023, 1, 1);
System.out.println(c1.get(Calendar.YEAR)+"-"+c1.get(Calendar.MONTH)+"-"+c1.get(Calendar.DATE));//2023-1-1
c1.set(2023, 1, 0);
System.out.println(c1.get(Calendar.YEAR)+"-"+c1.get(Calendar.MONTH)+"-"+c1.get(Calendar.DATE));//2023-0-31
}
7.5 File类
java.io.File类:文件和目录路径名的抽象表示形式
一、File类的常见构造方法:以pathname为路径创建File对象,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储
public File(String pathname)
二、通过File对象创建空文件或目录(在该对象所指的文件或目录不存在的情况下)
public boolean createNewFile()throws IOException
public boolean delete()
public boolean mkdir()
三、常用方法
import java.io.File;
import java.io.IOException;
public class TestFile {
/**
* File文件类 1.代表文件 2.代表目录
*/
public static void main(String[] args) {
File f = new File("d:/src3/TestObject.java");
File f2 = new File("d:/src3");
File f3 = new File(f2, "TestFile.java");
File f4 = new File(f2, "TestFile666.java");
File f5 = new File("d:/src3/aa/bb/cc/dd");
//f5.mkdirs();
f5.delete();
try {
f4.createNewFile();
System.out.println("文件创建成功!");
}catch(IOException e) {
e.printStackTrace();
}
if (f.isFile()) {
System.out.println("是一个文件!");
}
if (f2.isDirectory()) {
System.out.println("是一个目录!");
}
if (f3.isFile()) {
System.out.println("是一个文件奥");
}
7.6 String类
String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。 字符串是常量,它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。String底层使用一个字符数组来维护的,因为 String 对象是不可变的,所以可以共享。
7.6.1创建字符串对象方式
一、直接赋值方式创建对象是在方法区的常量池
String str="XiaoGuo";//直接赋值的方式
二、通过构造方法创建字符串对象是在堆内存
String str=new String("XiaoGuo");//实例化的方式
三、两种创建方式的比较
-
直接赋值(String str = “hello”)
只开辟一块堆内存空间,并且会自动入池,不会产生垃圾。
-
构造方法(String str= new String(“hello”);)
会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();方法进行手工入池
3. 在开发的过程中不会采用构造方法进行字符串的实例化
public static void main(String[] args) {
String str1 = "XiaoGuo";
String str2 = new String("XiaoGuo");
String str3 = str2; //引用传递,str3直接指向st2的堆内存地址
String str4 = "XiaoGuo";
/**
* ==:
* 基本数据类型:比较的是基本数据类型的值是否相同
* 引用数据类型:比较的是引用数据类型的地址是否相同
* 所以在这里的话:String类对象==比较,比较的是地址,而不是内容,equals则比较的是值
*/
System.out.println(str1==str2);//false
System.out.println(str1==str3);//false
System.out.println(str3==str2);//true
System.out.println(str1==str4);//true
}
7.6.2 常用的方法
7.6.2.1 String的判断
/*常用方法*/
boolean equals(Object obj)//比较字符串的内容是否相同
boolean equalsIgnoreCase(String str)//比较字符串的内容是否相同,忽略大小写
boolean startsWith(String str)//判断字符串对象是否以指定的str开头
boolean endsWith(String str)//判断字符串对象是否以指定的str结尾
public static void main(String[] args) {
// 创建字符串对象
String s1 = "hello";
String s2 = "hello";
String s3 = "Hello";
// boolean equals(Object obj):比较字符串的内容是否相同
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s3)); //false
// boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
System.out.println(s1.equalsIgnoreCase(s2)); //true
System.out.println(s1.equalsIgnoreCase(s3)); //true
// boolean startsWith(String str):判断字符串对象是否以指定的str开头
System.out.println(s1.startsWith("he")); //true
System.out.println(s1.startsWith("ll")); //false
}
7.6.2.2 String的截取
/*常用方法*/
int length()//获取字符串的长度,其实也就是字符个数
char charAt(int index)//获取指定索引处的字符
int indexOf(String str)//获取str在字符串对象中第一次出现的索引
String substring(int start)//从start开始截取字符串
String substring(int start,int end)//从start开始,到end结束截取字符串。包括start,不包括end
public static void main(String args[]) {
// 创建字符串对象
String s = "helloworld";
// int length():获取字符串的长度,其实也就是字符个数
System.out.println(s.length()); //10
// char charAt(int index):获取指定索引处的字符
System.out.println(s.charAt(0)); //h
System.out.println(s.charAt(1)); //e
// int indexOf(String str):获取str在字符串对象中第一次出现的索引
System.out.println(s.indexOf("l")); //2
System.out.println(s.indexOf("owo")); //4
System.out.println(s.indexOf("ak")); //-1
// String substring(int start):从start开始截取字符串
System.out.println(s.substring(0)); //helloworld
System.out.println(s.substring(5)); //world
// String substring(int start,int end):从start开始,到end结束截取字符串
System.out.println(s.substring(0, s.length())); //helloworld
System.out.println(s.substring(3, 8)); //lowor
}
7.6.2.3 String的转换
/*常用方法*/
char[] toCharArray()//把字符串转换为字符数组
String toLowerCase()//把字符串转换为小写字符串
String toUpperCase()//把字符串转换为大写字符串
public static void main(String args[]) {
// 创建字符串对象
String s = "abcde";
// char[] toCharArray():把字符串转换为字符数组
char[] chs = s.toCharArray();
for (int x = 0; x < chs.length; x++) {
System.out.println(chs[x]);
}
// String toLowerCase():把字符串转换为小写字符串
System.out.println("HelloWorld".toLowerCase());
// String toUpperCase():把字符串转换为大写字符串
System.out.println("HelloWorld".toUpperCase());
}
7.6.2.4 去除空格与分割
/*常用方法*/
String trim()//去除字符串两端空格
String[] split(String str)//按照指定符号分割字符串
public static void main(String args[]) {
// 创建字符串对象
String s1 = "helloworld";
String s2 = " helloworld ";
String s3 = " hello world ";
System.out.println("---" + s1 + "---");
System.out.println("---" + s1.trim() + "---");
System.out.println("---" + s2 + "---");
System.out.println("---" + s2.trim() + "---");
System.out.println("---" + s3 + "---");
System.out.println("---" + s3.trim() + "---");
// String[] split(String str)
// 创建字符串对象
String s4 = "aa,bb,cc";
String[] strArray = s4.split(",");
for (int x = 0; x < strArray.length; x++) {
System.out.println(strArray[x]);
}
}
7.6.3 String的不可变性
一、当我们去阅读源代码的时候,会发现有这样的一句话:意思就是说:String是个常量,从一出生就注定不可变,String类被fifinal修饰,官方注释说明创建后不能被改变,但是为什么String要使用fifinal修饰呢?
Strings are constant; their values cannot be changed after they are created.
二、原因是:因为String太过常用,JAVA类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。需要说明一点的是,在object中,equals()是用来比较内存地址的,但是String重写了equals()方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回true,
三、String不可变的好处:
1.可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销
2.我们的程序中大量使用了String字符串,有可能是出于安全性考虑
3.大家都知道HashMap中key为String类型,如果可变将变的多么可怕
4.当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定损失
7.6.4 字符串常量池
1. 常量池表(Constant_Pool table)
Class文件中存储所有常量(包括字符串)的table。这是Class文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是Class文件中的字节码指令。
2. 运行时常量池(Runtime Constant Pool)
JVM内存中方法区的一部分,这是运行时的内容。这部分内容(绝大部分)是随着JVM运行时候,从常量池转化而来,每个Class对应一个运行时常量池。上一句中说绝大部分是因为:除了 Class中常量池内容,还可能包括动态生成并加入这里的内容。
3. 字符串常量池(String Pool)
这部分也在方法区中,但与Runtime Constant Pool不是一个概念,String Pool是JVM实例全局共享的,全局只有一个。JVM规范要求进入这里的String实例叫“被驻留的interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留。
7.6.5 StringBuilder和StringBuffer
一、StringBuilder 是一个可变的字符序列。它继承于AbstractStringBuilder,实现了CharSequence接口。
二、StringBuffer 也是继承于AbstractStringBuilder的子类;但是,StringBuilder和StringBuffer不同,前者是非线程安全的,后者是线程安全的。
三、常用方法
7.6.5.1 insert
/**
* StringBuilder的insert()示例
*/
private static void testInsertAPIs(){
StringBuilder sbuilder = new StringBuilder();
// 在位置0处插入字符数组
sbuilder.insert(0, new char[]{'a', 'b', 'c', 'd', 'e'});
// 在位置0处插入字符数组。0表示字符数组起始位置,3表示长度
sbuilder.insert(0, new char[]{'A', 'B', 'C', 'D', 'E'}, 0, 3);
// 在位置0处插入float
sbuilder.insert(0, 1.414f);
// 在位置0处插入double
sbuilder.insert(0, 3.14159d);
// 在位置0处插入boolean
sbuilder.insert(0, true);
// 在位置0处插入char
sbuilder.insert(0, '\n');
// 在位置0处插入int
sbuilder.insert(0, 100);
// 在位置0处插入long
sbuilder.insert(0, 12345L);
// 在位置0处插入StringBuilder对象
sbuilder.insert(0, new StringBuilder("StringBuilder"));
// 在位置0处插入StringBuilder对象。6表示被在位置0处插入对象的起始位置(包括),13是结束位置(不包括)
sbuilder.insert(0, new StringBuilder("STRINGBUILDER"), 6, 13);
// 在位置0处插入StringBuffer对象。
sbuilder.insert(0, new StringBuffer("StringBuffer"));
// 在位置0处插入StringBuffer对象。6表示被在位置0处插入对象的起始位置(包括),12是结束位置(不包括)
sbuilder.insert(0, new StringBuffer("STRINGBUFFER"), 6, 12);
// 在位置0处插入String对象。
sbuilder.insert(0, "String");
// 在位置0处插入String对象。1表示被在位置0处插入对象的起始位置(包括),6是结束位置(不包括)
sbuilder.insert(0, "0123456789", 1, 6);
sbuilder.insert(0, '\n');
------------------------------------------
HashMap map = new HashMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
// 在位置0处插入Object对象。此处以HashMap为例
sbuilder.insert(0, map);
System.out.printf("%s\n\n", sbuilder);
}
7.6.5.2 append
/**
* StringBuilder的append()示例
*/
private static void testAppendAPIs() {
StringBuilder sbuilder = new StringBuilder();
// 追加字符数组
sbuilder.append(new char[]{'a','b','c','d','e'});
// 追加字符数组。0表示字符数组起始位置,3表示长度
sbuilder.append(new char[]{'A','B','C','D','E'}, 0, 3);
// 追加float
sbuilder.append(1.414f);
// 追加double
sbuilder.append(3.14159d);
// 追加boolean
sbuilder.append(true);
// 追加char
sbuilder.append('\n');
// 追加int
sbuilder.append(100);
// 追加long
sbuilder.append(12345L);
// 追加StringBuilder对象
sbuilder.append(new StringBuilder("StringBuilder"));
// 追加StringBuilder对象。6表示被追加对象的起始位置(包括),13是结束位置(不包括)
sbuilder.append(new StringBuilder("STRINGBUILDER"), 6, 13);
// 追加StringBuffer对象。
sbuilder.append(new StringBuffer("StringBuffer"));
// 追加StringBuffer对象。6表示被追加对象的起始位置(包括),12是结束位置(不包括)
sbuilder.append(new StringBuffer("STRINGBUFFER"), 6, 12);
// 追加String对象。
sbuilder.append("String");
// 追加String对象。1表示被追加对象的起始位置(包括),6是结束位置(不包括)
sbuilder.append("0123456789", 1, 6);
sbuilder.append('\n');
// 追加Object对象。此处以HashMap为例
HashMap map = new HashMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
sbuilder.append(map);
sbuilder.append('\n');
// 追加unicode编码
sbuilder.appendCodePoint(0x5b57); // 0x5b57是“字”的unicode编码
sbuilder.appendCodePoint(0x7b26); // 0x7b26是“符”的unicode编码
sbuilder.appendCodePoint(0x7f16); // 0x7f16是“编”的unicode编码
sbuilder.appendCodePoint(0x7801); // 0x7801是“码”的unicode编码
System.out.printf("%s\n\n", sbuilder);
}
7.6.5.3 replace
/**
* StringBuilder的replace()示例
*/
private static void testReplaceAPIs() {
StringBuilder sbuilder;
sbuilder = new StringBuilder("0123456789");
sbuilder.replace(0, 3, "ABCDE");
System.out.printf("sbuilder=%s\n", sbuilder);
sbuilder = new StringBuilder("0123456789");
sbuilder.reverse();
System.out.printf("sbuilder=%s\n", sbuilder);
sbuilder = new StringBuilder("0123456789");
sbuilder.setCharAt(0, 'M');
System.out.printf("sbuilder=%s\n", sbuilder);
System.out.println();
}
7.6.5.4 delete
/**
* StringBuilder的delete()示例
*/
private static void testDeleteAPIs() {
StringBuilder sbuilder = new StringBuilder("0123456789");
// 删除位置0的字符,剩余字符是“123456789”。
sbuilder.deleteCharAt(0);
// 删除位置3(包括)到位置6(不包括)之间的字符,剩余字符是“123789”。
sbuilder.delete(3,6);
// 获取sb中从位置1开始的字符串
String str1 = sbuilder.substring(1);
// 获取sb中从位置3(包括)到位置5(不包括)之间的字符串
String str2 = sbuilder.substring(3, 5);
// 获取sb中从位置3(包括)到位置5(不包括)之间的字符串,获取的对象是CharSequence对象,此处转型为String
String str3 = (String)sbuilder.subSequence(3, 5);
System.out.printf("sbuilder=%s\nstr1=%s\nstr2=%s\nstr3=%s\n",
sbuilder, str1, str2, str3);
}
7.5.4.5 index
/**
* StringBuilder中index示例
*/
private static void testIndexAPIs() {
StringBuilder sbuilder = new StringBuilder("abcAbcABCabCaBcAbCaBCabc");
System.out.printf("sbuilder=%s\n", sbuilder);
// 1. 从前往后,找出"bc"第一次出现的位置
System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\")",
sbuilder.indexOf("bc"));
// 2. 从位置5开始,从前往后,找出"bc"第一次出现的位置
System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\", 5)",
sbuilder.indexOf("bc", 5));
// 3. 从后往前,找出"bc"第一次出现的位置
System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\")",
sbuilder.lastIndexOf("bc"));
// 4. 从位置4开始,从后往前,找出"bc"第一次出现的位置
System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\", 4)",
sbuilder.lastIndexOf("bc", 4));
System.out.println();
}
7.5.4.6 其他API
/*
capacity()返回的是字符串缓冲区的容量
StringBuffer(); 分配16个字符的缓冲区
StringBuffer(int len); 分配len个字符的缓冲区
StringBuffer(String s); 除了按照s的大小分配空间外,再分配16个字符的缓冲区你的StringBuffer是用字符构造的,"0123456789"的长度是10另外再分配16个字符,所以一共是26
*/
private static void testOtherAPIs() {
StringBuilder sbuilder = new StringBuilder("0123456789");
//字符串缓冲区的容量
int cap = sbuilder.capacity();
System.out.printf("cap=%d\n", cap);
char c = sbuilder.charAt(6);
System.out.printf("c=%c\n", c);
char[] carr = new char[4];
sbuilder.getChars(3, 7, carr, 0);
for (int i=0; i<carr.length; i++){
System.out.printf("carr[%d]=%c ", i, carr[i]);
}
System.out.println();
}
7.5.4.7 总结
一、首先需要说明的是:
1.String是字符串常量
2.StringBuffer是字符串变量(线程安全)
3.StringBuilder是字符串变量(非线程安全)
4.在大多数情况下三者在执行速度方面的比较:StringBuilder > StringBuffer > String
二、对于三者使用的总结:
1.如果要操作少量的数据用String
2.单线程操作字符串缓冲区下操作大量数据StringBuilder
3.多线程操作字符串缓冲区下操作大量数据StringBuffffer
三、关于运行效率的解释:
1.String类型是不可改变的,每次修改相当重新创建指针指向新生成的String对象
2.经常改变的字符串不要使用String,生成过多时,JVM的GC会工作,从而影响速度
3.StringBuffer每次改变则对本身进行操作,而不是生成新的对象,再改变对象引用
4.所以对于字符串经常改变的情况下,推荐使用StringBuffer
四、特殊情况下String要优于StringBuffer
在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中,String 效率是远要比 StringBuffer快的:
//原因是在JVM眼里,这个String S1 = “This is only a”+ “simple”+“test”;其实就是:String S1 = “This is only a simple test”;
String S1 = “This is only a”+“simple” + “test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“simple”).append(“test”);
//但是如果对不同的String对象操作时,StringBuffer速度要优于String
String S2 = “This is only a”;
String S3 = “simple”;
String S4 = “test”;
7.6.6 String、StringBuffer、StringBuilder区别
1.StringBuilder效率高,线程不安全
2.StringBuffer效率低,线程安全
3.String是不可变字符串,StringBuilder是可变字符串。为什么有这样的差异,可以深入源码去解析,比如String类内的 priver fifinal char value[] 等方法的原因
4.如果是简单的声明一个字符串没有后续过多的操作,使用String,StringBuilder均可,若后续对字符穿做频繁的添加,删除操作,或者是在循环当中动态的改变字符穿的长度应该用StringBuilder。使用String会产生多余的字符串,占用内存空间。
7.7 包装类
7.7.1 介绍
Java 语言是典型的面向对象编程语言,但其中的八种基本数据类型并不支持面向对象编程,基本类型的数据不具备“对象”的特性——不携带属性、没有方法可调用。Java为每种基本数据类型分别设计了对应的类,称之为包装类,每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的方法。包装类对象一经创建,其封装的基本类型数据值不可改变
基本数据类型 | 对应包装类型 |
---|---|
byte | Byte |
short | Short |
long | Long |
int | Integer |
float | Float |
char | Character |
boolean | Boolean |
double | Double |
7.7.2 装箱与拆箱
基本类型和对应的包装类可以相互转换
一、装箱
由基本类型向对应的包装类转换,例如把int包装成 Integer 类的对象
二、拆箱
包装类向对应的基本类型转换,例如把Integer类的对象重新简化为int
三、自动拆箱与装箱
JDK1.5之前需要手动装箱拆箱,1.5之后可以自动进行装箱与拆箱,自动装箱与拆箱的功能事实上是编译器来帮您的忙,编译器在编译时期依您所编写的语法,决定是否进行装箱或拆箱动作,所有的包装类都是抽象类Number 的子类
Integer i = 100;
//相当于编译器自动为您作以下的语法编译:
Integer i = new Integer(100);
public static void main(String[] args) {
int m = 500;
Integer obj = m; // 自动装箱
int n = obj; // 自动拆箱
System.out.println(n);//500
Integer obj1 = 500;
System.out.println(obj.equals(obj1));//true
}
8.JavaSE:异常机制
8.1 异常体系结构
Java把异常当作对象来处理,并定义一个基类 java.lang.Throwable 作为所有异常的父类;Throwable被分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误**。另一个分支是Exception**,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。Exception 又分为运行时异常( RuntimeException )和非运行时异常
8.2 异常之间的区别与联系
一、Error
Error类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关;是程序无法控制和处理的,当出现这些异常时,JVM一般会选择终止线程
二、Exception
1. 运行时异常:这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;如NullPointerException、ArithmeticException、 MissingResourceException、ClassNotFoundException、ArrayIndexOutOfBoundsException、等异常
2.非运行时异常:从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException 、 SQLException 等以及用户自定义的 Exception 异常
8.3 检查异常和不受检查异常
一、检查异常
在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理
分析:除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过
二、不受检查异常
RuntimeException及其子类和Error
分析: 不受检查异常为编译器不要求强制处理的异常,
8.4 异常处理机制
8.4.1 抛出异常
假使我们创建了一个学生对象Student的一个引用stu,在调用的时候可能还没有初始化;所以在使用这个对象引用调用其他方法之前,要先对它进行检查,可以创建一个代表错误信息的对象,并且将它从当前环境中抛出,这样就把错误信息传播到更大的环境中
if(stu == null){
throw new NullPointerException();
}
8.4.2 捕获异常
由于运行时异常及其子类的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常
8.4.3 处理异常
8.4.3.1 try-catch
public class TestException {
public static void main(String[] args) {
int a = 1;
int b = 0;
try { //try监控区域
if (b == 0) throw new ArithmeticException(); //通过throw语句抛出异常
System.out.println("a/b的值是:" + a / b);
}
catch (ArithmeticException e) { //catch捕捉异常
System.out.println("程序出现异常,变量b不能为0!");
}
System.out.println("程序正常结束。");
}
}
8.4.3.2 throw
try{
//.....throw new XXXXXXXException()
}
catch(XXXXXXXException e){
}
8.4.3.3 throws
public void info() throws Exception
{
//...
}
8.4.3.4 finally
//finally子句是可选项,可以有也可以无,但是每个try语句至少需要一个catch或者finally子句
//finally正常情况下一定执行,特殊情况下不一定会执行
try{
System.out.println("inside proc2");
return;
}finally{
System.out.println("proc2's finally");
}
8.4.3.5 执行顺序
1.执行try、catch给返回值赋值
2.执行finally
3.return
8.4.4 自定义异常
//自定义异常
public class MyException extends Exception {
MyException(int a){
}
public class Action{
//抛出异常
public void actionException throws MyException(){
//.....
}
public static void main(String[] args){
try{
//.....
}
catch(MyException e){
//.....
}
}
}
8.4.5 总结
1.多重try-catch后可以加一个catch(Exception)来避免被遗漏的异常
2.尽量添加finally语句来释放被占用的资源
3.尽量去处理异常,而不是使用printStackTrace()打印输出
9.JavaSE:集合框架
9.1 概述
1.Collection 接口存储一组不唯一,无序的对象
2.List 接口存储一组不唯一,有序的对象
3.Set 接口存储一组唯一,无序的对象
4.Map 接口存储一组键值对象,提供key到value的映射
5.LinkedList采用链表存储方式,插入、删除元素时效率比较高
5.HashSet的底层是用HashMap实现的,因此查询效率较高,由于采用hashCode算法直接确定元素的内存地址,增删效率也挺高的
9.2 ArrayList
ArrayList实现了长度可变的数组,在内存中分配连续的空间所以遍历元素和随机访问元素的效率比较高
9.2.1 ArrayList实践
//我们现在有4名学生,存储它的信息,获取总数,并能够逐条打印信息
public class Student {
/*提供构造方法、Get、Set方法*/
public Integer ID;
public String name;
public double sorce;
}
--------------------------------------------------------------------------------------
public static void main(String[] args) {
List arrylist = new ArrayList();
//添加信息
arrylist.add(new Student("XiaoGuo", 20230426, 110.5));
arrylist.add(new Student("XiaoLiu", 20230425, 105.3));
arrylist.add(new Student("XiaoZhang", 20230423, 100));
//指定位置添加
arrylist.add(2,new Student("XiaoSun", 20230422, 105.4));
//size()用于返回ArrayList长度
System.out.println("学生共有:" + arrylist.size());
//遍历数组
for ( Object x: arrylist) {
Student student = (Student)x;
System.out.println("学号"+student.ID+" "+"姓名"+student.name+" "+"成绩"+student.sorce );
}
}
//学生共有:4
//学号20230426 姓名XiaoGuo 成绩110.5
//学号20230425 姓名XiaoLiu 成绩105.3
//学号20230422 姓名XiaoSun 成绩105.4
//学号20230423 姓名XiaoZhang 成绩100.0
9.2.2 ArrayList常用方法
返回值类型 | 方法 |
---|---|
boolean | add(E e):将指定的元素追加到此列表的末尾 |
void | add(int index, E element):在此列表中的指定位置插入指定的元素。 |
E | get(int index):返回此列表中指定位置的元素 |
Iterator | iterator() :以正确的顺序返回该列表中的元素的迭代器 |
int | lastIndexOf(Object o):返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1 |
boolean | remove(int index) :删除该列表中指定位置的元素 |
boolean | removeAll(Collection<?> c):从此列表中删除指定集合中包含的所有元素 |
int | size():返回此列表中的元素个数 |
void | sort(Comparator<? super E> c):使用提供的Comparator对此列表进行排序以比较元素 |
Object | toArray():以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组 |
boolean | contains(object)如果此列表包含指定的元素,则返回 true 。 |
9.2.3 ArrayList源码分析
我们对ArrayList类的实例的所有的操作底层都是基于数组的,底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据;
9.2.3.1 继承关系
ArrayList **extends** AbstractList;AbstractList **extends** AbstractCollection
一、为什么要先继承AbstractList,而让AbstractList先实现List?而不是让ArrayList直接实现List?
这里是有一个思想,接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。所以一般看到一个类上面还有一个抽象类
二、ArrayList实现了哪些接口?
1.List接口:由于编写失误,使ArrayList的父类AbstractList也实现了List接口
**2.RandomAccess:**标记性接口,通过查看API文档,它的作用是用来快速随机存取,提高获取数据的效率;如实现该接口的ArrayList使用普通的for循环来 遍历,效率更高;而LinkedList没有实现该接口,所以使用Iterator来迭代,这样效率更高;所以这个标记只是为了让我们知道用什么方 式获取数据性能更好
**3.Cloneable:**实现了该接口,就可以使用Object.Clone()方法了
**4.Serializable:**实现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原来的类
9.2.3.2 类中属性与构造方法
//类中属性
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 缺省容量
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 缺省空对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组
transient Object[] elementData;
// 实际元素大小,默认为0
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
//无参构造方法
//Constructs an empty list with an initial capacity of ten.这里就说明了默认会给10的大小,所以说一开始arrayList的容量是10
//ArrayList中储存数据的其实就是一个数组,这个数组就是elementData
public ArrayList() {
super(); //调用父类中的无参构造方法,父类中的是个空的构造方法
//EMPTY_ELEMENTDATA:是个空的Object[]将elementData初始化,elementData也是个Object[]类型,空的Object[]会给默认大小10
this.elementData = EMPTY_ELEMENTDATA;
}
//有参构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
将自定义的容量大小当成初始化 initialCapacity 的大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; //等同于无参构造方法
} else {
//判断如果自定义大小的容量小于0,则报下面这个非法数据异常
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
9.2.3.3 核心方法
一、boolean add(E)
//添加一个特定元素到list末尾
public boolean add(E e) {
//size是数组中数据的个数,因为要添加一个元素,所以size+1
//先判断size+1的这个数数组能否放得下,就在这个方法中去判断是否数组.length是否够用了。
ensureCapacityInternal(size + 1); //确定内部容量是否够
elementData[size++] = e; //在数据中正确的位置上放上元素e,并且size++
return true;
}
二、void add(int ,E)
public void add(int index, E element) {
//检查index也就是插入的位置是否合理。
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);//确定内部容量是否够
//这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位,
System.arraycopy(elementData, index, elementData, index + 1,size - index);
//在目标位置上存放元素
elementData[index] = element;
size++;
}
三、 remove(int)
//删除指定位置上的元素
//arrayList是可以存放null值
public E remove(int index) {
rangeCheck(index);//检查index的合理性
modCount++;//这个作用很多,比如用来检测快速失败的一种标志。
E oldValue = elementData(index);//通过索引直接找到该元素
int numMoved = size - index - 1;//计算要移动的位数
if (numMoved > 0)
//这个方法也已经解释过了,就是用来移动元素的
System.arraycopy(elementData, index+1, elementData, index,numMoved);
//将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
elementData[--size] = null; // clear to let GC do its work
//返回删除的元素。
return oldValue;
}
四、clean()
//通过将elementData中每个元素都赋值为null,等待GC回收
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
五、removeAll(collection c)
remove函数,用户移除指定下标的元素,会把指定下标到数组末尾的元素向前移动一个单位,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC
public boolean removeAll(Collection<?> c) {
return batchRemove(c, false);//批量删除
}
六、set()
//设定指定下标索引的元素值
public E set(int index, E element) {
// 检验索引是否合法
rangeCheck(index);
// 旧值
E oldValue = elementData(index);
// 赋新值
elementData[index] = element;
// 返回旧值
return oldValue;
}
七、indexOf()
从头开始查找与指定元素相等的元素,是可以查找null元素的!意味着ArrayList中可以存放null元素的;与此函数对应的lastIndexOf,表示从尾部开始查找
// 从首开始查找数组里面是否存在指定元素
public int indexOf(Object o) {
if (o == null) { // 查找的元素为空
for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标
if (elementData[i]==null)
return i;
} else { // 查找的元素不为空
for (int i = 0; i < size; i++) // 遍历数组,找到第一个和指定元素相等的元素,返回下标
if (o.equals(elementData[i]))
return i;
}
// 没有找到,返回空
return -1;
}
八、get()
//get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0)
public E get(int index) {
// 检验索引是否合法
rangeCheck(index);
return elementData(index);
}
-----------------------------------
//element函数用于返回具体的元素
E elementData(int index) {
return (E) elementData[index];//返回的值都经过了向下转型(Object -> E)
}
9.2.3.4 ArrayList总结
1.ArrayList可以存放null
2.ArrayList本质上就是一个elementData数组
3.ArrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法
4.ArrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是删除集合中的全部元素。
5.ArrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,有时需要移动很多数据才能达到应有的效果
6.ArrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环
9.3 LinkedList
9.3.1 LinkedList概述
1.LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的
2.LinkedList 是一个继承于AbstractSequentialList的双向链表,它也可以被当作堆栈、队列或双端队列进行操作
3.LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆
4.LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输
5.LinkedList 是非同步的
9.3.2 LinkedList数据结构
一、单向链表
1.element:用来存放元素;next:用来指向下一个节点元素
2.通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向nul
二、单向循环链表
1.element:用来存放元素;next:用来指向下一个节点元素
2.在单向链表的最后一个节点的next会指向头节点,而不是指向null,这样存成一个环
三、双向链表
1.element:存放元素;pre:用来指向前一个元素;next:指向后一个元素
2.双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null
四、双向循环链表
1.element、pre、next 跟前面的一样
2.第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”
9.3.3 LinkedList源码分析
9.3.3.1 类中属性与构造方法
public class LinkedList<E>extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{//该区域均由transient关键字修饰,所以序列化时候该区域不会被序列化
// 实际元素个数
transient int size = 0;
// 头结点
transient Node<E> first;
// 尾结点
transient Node<E> last;
}
//空参构造方法
public LinkedList(){
}
------------------------------------------------
//有参构造方法
//将集合c中的各个元素构建成LinkedList链表
public LinkedList(Collection<? extends E> c) {
// 调用无参构造函数
this();
// 添加集合中所有的元素
addAll(c);
}
9.3.3.2 内部类(Node)
内部类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;
}
}
9.3.3.3 核心方法
一、add()
//添加一个元素到链表尾部
public boolean add(E e) {
// 添加到末尾
linkLast(e);
return true;
}
二、remove(Object o)
//如果我们要移除的值在链表中存在多个一样的值,那么我们会移除index最小的那个,也就是最先找到的那个值,如果不存在这个值,那么什么也不做
public boolean remove(Object o) {
//这里可以看到,linkedList也能存储null
if (o == null) {
//循环遍历链表,直到找到null值,然后使用unlink移除该值,下面的这个else中也一样
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {,
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
9.3.3.4 LinkedList迭代器
在LinkedList中除了有一个Node的内部类外,应该还能看到另外两个内部类,那就是ListItr,还有一个是DescendingIterator
//ListItr内部类
private class ListItr implements ListIterator<E> {
//ListIterator中不止有向后迭代的方法,还有向前迭代的方法,所以这个内部类能让linkedList不光能像后迭代,也能向前迭代
}
//DescendingIterator内部类
private class DescendingIterator implements Iterator<E> {
//调用的ListItr,作用是封装一下Itr中几个方法,让使用者以正常的思维去写代码,例如,在从后往前遍历的时候,也是跟从前往后遍历一样,使用next等操作,而不用使用特殊的previous
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
9.3.4 总结
1.异步,也就是非线程安全
2.底层采用双向链表实现,通过一个Node内部类实现的这种链表结构,不存在容量不足的情况
3.是顺序存取结构
4.没有实现RandomAccess,所以使用foreach或者iterator都可以
5.能存储null值
6.跟ArrayList相比较,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好
7.由于实现了list和Deque接口,能够当作队列来使用
8.ListItr内部类提供向前迭代,还能向后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值
9.DescendingIterato内部类使其从后往前迭代时,仍能使用next()操作
9.4 Vector和Stack
一、Vector是一个可变化长度的数组,Vector是一个线程安全的类,如果使用需要线程安全就使用Vector,如果不需要,就使用arrayList
1.add()
//在方法前面加了synchronized关键字,给该方法加锁了,哪个线程先调用它,其它线程就得等着
public synchronized boolean add(E e) {
modCount++;
//通过arrayList的源码分析经验,这个方法应该是在增加元素前,检查容量是否够用
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.grow(int)
//看一下这个方法,其实跟arrayList一样,唯一的不同就是在扩增数组的方式不一样
//如果capacityIncrement不为0,那么增长的长度就是capacityIncrement,如果为0,那么扩增为2倍的原容量
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
二、现在来看看Vector的子类Stack,就是栈的意思;那么该类就是跟栈的用法一样,包括出栈,入栈等,构造方法也是空的,用的还是数组,父类中的构造,跟父类一样的扩增方式,并且它的方法也是同步的,所以也是线程安全
class Stack<E> extends Vector<E> {}
三、Vector和Stack总结
【Vector总结】
1.Vector线程安全是因为它的方法都加了synchronized关键字
2.Vector的本质是一个数组,特点能是能够自动扩增,扩增的方式跟capacityIncrement的值有关
3.也会fail-fast
【Stack的总结】
1.对栈的一些操作,先进后出
2.底层也是用数组实现的,因为继承了Vector
3.也是线程安全的
9.5 List总结
一、为什么不提倡使用Vector了?
1.vector实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的
2.如果多个Thread并发执行一个已经加锁的方法,但是在该方法中,又有vector的存在,会导致锁上又加锁,会造成额外的开销
3.vector还有fail-fast的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁
4.在JDK1.5之后就被弃用了,现在如果要用到线程安全的集合,都是从java.util.concurrent包下去拿相应的类
二、ArrayList和LinkedList区别
【不同处】
1.ArrayList底层是用数组实现的顺序表,是随机存取类型,可自动扩增
2.ArrayList在初始化时,数组的长度是0,只有在增加元素时,长度才会增加。默认是10,不能无限扩增,有上限,在查询操作的时候性能更好
3.LinkedList底层是用链表来实现的,是一个双向链表,顺序存取类型,理论上可无限增加
【相同处】
两个都是线程不安全的,在iterator时,会发生fail-fast:快速失效
三、fail-fast和fail-safe
简单的来说:在java.util下的集合都是发生fail-fast,而在java.util.concurrent下的发生的都是failsafe
1.【fail-fast】
快速失败,例如在arrayList中使用迭代器遍历时,有另外的线程对arrayList的存储数组进行了改变,比如add、delete、等使之发生了结构上的改变,所以Iterator就会快速报一个java.util.ConcurrentModifificationException 异常(并发修改异常),这就是快速失败
2.【fail-safe】
安全失败,在java.util.concurrent下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行结构的改变,不会报异常,而是正常遍历,这就是安全失败;原理是在添加操作时会创建副本,在副本上进行添加操作,等迭代器遍历结束后,会将原引用改为副本引用
四、为什么在java.util.concurrent包下对集合有结构的改变,却不会报异常?
1.在concurrent下的集合类增加元素的时候使用Arrays.copyOf()来拷贝副本
2.如果有其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合
3.迭代器还是照常遍历,遍历完之后,改变原引用指向副本
五、vector也是线程安全的,为什么是fail-fast呢?
并不是说线程安全的集合就不会报fail-fast,而是报fail-safe;而像ArrayList、LinkedList、Verctor等,他们底层就是对着真正的引用进行操作,而不是创建副本后对副本进行操作,所以才会发生异常
六、既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢?
首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿vector来说,我们都没有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办法,因为那把锁就放在那边
9.6 HashMap
HashMap是基于哈希表的Map接口实现的,它存储的是内容是键值对<key,value>映射。此类不保证映射的顺序,假定哈希函数将元素适当的分布在各桶(在数组中每个位置上放一个桶装元素)之间,可为基本操作(get和put)提供稳定的性能
9.6.1 HashMap数据结构
一、HashMap在JDK1.8以前数据结构和存储原理
1.数据结构:链表散列,由链表与数组构成
2.存储原理:
第一步:HashMap内部有一个entry的内部类,其中有四个属性,我们要存储一个值,则需要一个key和一个value,存到map中就会先将key和value 保存在这个Entry类创建的对象中
第二步:通过key、value封装成一个Entry对象,然后通过key的值来计算该Entry的hash值,通过Entry的hash值和数组的长度length来计算出Entry放 在数组中的哪个位置上面,如果在这个位置上还有其他元素,则通过链表来存储这个元素
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //key
V value; //value值
Entry<K,V> next;//指向下一个entry对象
int hash;//通过key算过来的你hashcode值
}
二、HashMap在JDK1.8以后的数据结构和存储原理
采用数组+链表+红黑树的形式,其中红黑树的引用,可以提高效率
9.6.2 HashMap属性
一、影响HashMap的性能因素包括
1.初始化容量:一个初始化容量仅仅是在哈希表被创建时容量
2.加载因子:当entry的数量在哈希表中超过了加载因子乘以当前的容量,则哈希表内部的数据结构会被重新建立,最终哈希表内有大约两倍的桶的数量
//Java中默认初始容量为16,加载因子为0.75
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
二、loadFactor加载因子
1.加载因子用来衡量HashMap的稀疏程度,其中loadFactor默认值为0.75f
2.计算HashMap的实时装载因子的方法为:数组长度/初始化容量,而不是以使用的数组数量去除以初始容量
3.loadFactor越趋近于1,那么数组中存放的数据(entry)也就越密
4.loadFactor越趋近于0,那么数组中存放的数据(entry)也就越稀
5.如果将loadFactor直接设置为1,通过key拿value时,是先通过key的hashCode找到对应数组的位置;如果该位置有很多元素,则需要通过equals来比较链表 中的元素,拿到我们的value值,所以花费的性能就很高
三、桶
数组中每一个位置上都放有一个桶,每个桶里就是装一个链表,链表中可以有很多个元素(entry),也就相当于把元素都放在桶中
四、capacity
capacity译为容量代表的数组的容量,也就是数组的长度,同时也是HashMap中桶的个数,默认值是16,一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂
五、size的含义
size就是在该HashMap的实例中实际存储的元素的个数
六、threshold的含义
threshold = capacity * loadFactor,当Size>=threshold的时候,并且对应数组位置上有元素,那么就要考虑对数组的扩增;也就是说,是衡量数组是否需要扩增的一个标准
int threshold;
七、modCount
modCount字段用来记录HashMap内部结构发生变化的次数
transient int modCount;
9.6.3 HashMap源码分析
一、继承结构
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
// Cloneable:能够使用Clone()方法,在HashMap中,实现的是浅层次拷贝,即对拷贝对象的改变会影响被拷贝的对象
// Serializable:能够使之序列化,即可以将HashMap对象保存至本地,之后可以恢复状态
}
二、HashMap类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
三、HashMap的构造方法
//HashMap()
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//HashMap(int)
//构造一个空的HashMap具有指定的初始容量和默认负载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//HashMap(int,float)
//构造一个空的HashMap具有指定的初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0,否则报错
if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: "+initialCapacity);
// 初始容量不能大于最大值,否则为最大值
if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
// 填充因子不能小于或等于0,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: "+loadFactor);
// 初始化填充因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
//将m的所有元素存入本HashMap实例中
//构造一个新的HashMap与指定的相同的映射Map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) { // pre-size
// 未初始化,s为m的实际元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
// 计算得到的t大于阈值,则初始化阈值
if (t > threshold)threshold = tableSizeFor(t);
}
// 已初始化,并且m元素个数大于阈值,进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
//HashMap(int)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
9.6.4 常用方法
返回类型 | 方法 |
---|---|
void | clear():从Map中删除所有的映射 |
Object | clone():返回此HashMap实例的浅拷贝:键和值本身不被克隆 |
V | compute(…):尝试计算用于指定键和其当前映射的值的映射(或 null如果没有当前映射) |
V | computeIfAbsent(…) :如果指定的键尚未与值相关联或映射到 null,则尝试使用给定的映射函数计算其值,并将其输入到此映射中,除非 null |
V | computeIfPresent(…):如果指定的密钥的值存在且非空,则尝试计算给定密钥及其当前映射值的新映射 |
boolean | containsKey(Object key) :如果此映射包含指定键的映射,则返回true |
boolean | containsValue(Object value):如果此地图将一个或多个键映射到指定值,则返回true |
Set<Map.Entry<K,V>> | entrySet() :返回此地图中包含的映射的Set视图 |
void | forEach(…) : 对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常 |
V | get(Object key) : 返回到指定键所映射的值,或 null如果此映射包含该键的映射 |
V | getOrDefault(…) :返回到指定键所映射的值,或 defaultValue如果此映射包含该键的映射 |
boolean | isEmpty() :如果此地图不包含键值映射,则返回 true |
Set | keySet() :返回此地图中包含的键的Set视图 |
V | merge(…) :如果指定的键尚未与值相关联或与null相关联,则将其与给定的非空值相关联 |
V | put(K key, V value) :将指定的值与此映射中的指定键相关联 |
void | putAll(Map<? extends K,? extends V> m) : 将指定地图的所有映射复制到此地图 |
V | putIfAbsent(K , V ) :如果指定的键尚未与某个值相关联(或映射到 null ),则将其与给定值相关联并返回 null ,否则返回当前值 |
V | remove(Object key) :从该地图中删除指定键的映射(如果存在) |
boolean | remove(Object key, Object value) :仅当指定的密钥当前映射到指定的值时删除该条目 |
V | replace(K key, V value): 只有当目标映射到某个值时,才能替换指定键的条目 |
boolean | replace(K key, V oldValue, V newValue) :仅当当前映射到指定的值时,才能替换指定键的条目 |
void | replaceAll(…):将每个条目的值替换为对该条目调用给定函数的结果,直到所有条目都被处理或该函数抛出异常 |
int | size() :返回此地图中键值映射的数量 |
Collection | values() :返回此地图中包含的值的Collection视图 |
9.6.5 总结
public static void main(String[] args) {
HashMap<String,Student> map = new HashMap();
//添加元素
map.put("小郭01",new Student(1,"XiaoGuo01"));
map.put("小郭02",new Student(2,"XiaoGuo02"));
//获取元素
Student student = map.get("小郭01");
Student student02 = map.get("小郭02");
//输出元素
System.out.println("ID:"+ student.id +" "+"姓名:"+ student.name);
System.out.println("ID:"+ student02.id +" "+"姓名:"+ student02.name);
//获取集合大小
int size = map.size();
System.out.println("Map大小为:"+size);
//------------------------------------------------------
// 添加相同的key,会进行覆盖
// key和value都可以为null
map.put("小郭01",new Student(1,"已覆盖"));
//------------------------------------------------------
//一般根据得到的key进行遍历value,因为value值不一定唯一
//通过Set接口获取键值,遍历value
Set<String> set = map.keySet();
for (String key:set) {
System.out.println("获取的键为:"+ key + map.get(key));
}
//直接获取value集合
Collection<Student> values = map.values();
for (Student v: values) {
System.out.println("获取的value值为:"+ v );
}
//直接得到key与value的集合
Set<Map.Entry<String,Student>> setSecond = map.entrySet();
for (Map.Entry<String,Student> entry :setSecond) {
System.out.println(entry.getKey()+entry.getValue());
}
}
9.7 迭代器
一、所有实现了Collection接口的容器类都有一个iterator方法用以返回一个实现Iterator接口的对象,Iterator对象称作为迭代器,用以方便的对容器内元素的遍历操作,Iterator接口定义了如下方法:
1.boolean hashNext();//判断是否有元素没有被遍历
2.Object next();//返回游标当前位置的元素并将游标移动到下一个位置
3.void remove();//删除游标左边的元素,在执行完next之后该操作只能执行一次
二、通过迭代器Iterator实现遍历
1.获取Iterator :Collection 接口的iterator()方法
2.Iterator的方法:boolean hasNext(): 判断是否存在另一个可访问的元素;Object next(): 返回要访问的下一个元素
Set keys=dogMap.keySet(); //取出所有key的集合
Iterator it= keys.iterator(); //获取Iterator对象
while(it.hasNext())
{ String key=(String)it.next(); //取出key
Dog dog=(Dog)dogMap.get(key); //根据key取出对应的值
System.out.println(key+"\t"+dog.getStrain());
}
9.8 泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型;
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
//创建ArrayList集合对象
List<Student> student = new Arraylist<Student>();
//student.add("XiaoGuo");编译错误,无法转换
for(Student x:student){
//......无需强制类型转换
}
9.9 Collections工具类
Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类提供了大量方法对集合进行排序、查询和修改等操作,还提供了将集合对象置为不可变、对集合对象实现同步控制等方法。
此类完全由在 collection 上进行操作或返回 collection 的静态方法组成,这个类不需要创建对象,内部提供的都是静态方法
9.9.1 排序
//反转列表中元素的顺序
static void reverse(List<?> list)
//对List集合元素进行随机排序
static void shuffle(List<?> list)
//根据元素的自然顺序 对指定列表按升序进行排序
static void sort(List<T> list)
//根据指定比较器产生的顺序对指定列表进行排序
static <T> void sort(List<T> list, Comparator<? super T> c)
//在指定List的指定位置i,j处交换元素
static void swap(List<?> list, int i, int j)
//当distance为正数时,将List集合的后distance个元素“整体”移到前面
//当distance为负数时,将list集合的前distance个元素“整体”移到后边,该方法不会改变集合的长度
static void rotate(List<?> list, int distance)
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//集合元素的次序反转,输出:[6, -1, 5, 9, -2, 3]
Collections.reverse(list);
//排序:按照升序排序,输出:[-2, -1, 3, 5, 6, 9]
Collections.sort(list);
//根据下标进行交换,输出:[-2, -1, 9, 5, 6, 3]
Collections.swap(list, 2, 5);
//随机排序,每次输出的次序不固定
Collections.shuffle(list);
//后两个整体移动到前边,输出:[-1, 6, 3, -2, 9, 5]
Collections.rotate(list, 2);
}
}
9.9.2 查找、替换操作
//使用二分搜索法搜索指定列表,以获得指定对象在List集合中的索引
//注意:此前必须保证List集合中的元素已经处于有序状态
static <T> int binarySearch(List<? extends Comparable<? super T>>list, T key)
//根据元素的自然顺序,返回给定collection的最大元素
static Object max(Collection coll)
//根据指定比较器产生的顺序,返回给定collection的最大元素
static Object max(Collection coll,Comparator comp)
//根据元素的自然顺序,返回给定collection的最小元素
static Object min(Collection coll)
//根据指定比较器产生的顺序,返回给定collection的最小元素
static Object min(Collection coll,Comparator comp)
//使用指定元素替换指定列表中的所有元素
static <T> void fill(List<? super T> list, T obj)
//返回指定collection中等于指定对象的出现次数
static int frequency(Collection<?> c, Object o)
//返回指定源列表中第一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1
static int indexOfSubList(List<?> source, List<?> target)
//返回指定源列表中最后一次出现指定目标列表的起始位置;如果没有出现这样的列表,则返回-1
static int lastIndexOfSubList(List<?> source, List<?> target)
//使用一个新值替换List对象的所有旧值oldal
static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsTest1 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(3);
list.add(-2);
list.add(9);
list.add(5);
list.add(-1);
list.add(6);
//输出最大元素9
System.out.println(Collections.max(list));
//输出最小元素:-2
System.out.println(Collections.min(list));
//将list中的-2用1来代替 [3, 1, 9, 5, -1, 6]
System.out.println(Collections.replaceAll(list, -2, 1));
//判断9在集合中出现的次数,返回2
list.add(9);
System.out.println(Collections.frequency(list, 9));
//对集合进行排序 [-1, 1, 3, 5, 6, 9, 9]
Collections.sort(list);
//只有排序后的List集合才可用二分法查询,输出2
System.out.println(Collections.binarySearch(list, 3));
}
}
9.9.3 同步控制
Collectons提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题;正如前面介绍的HashSet,TreeSet,arrayList,LinkedList,HashMap,TreeMap都是线程不安全的,Collections提供了多个静态方法可以把他们包装成线程同步的集合
//返回指定collection支持的同步(线程安全的)collection
static <T> Collection<T> synchronizedCollection(Collection<T> c)
//返回指定列表支持的同步(线程安全的)列表
static <T> List<T> synchronizedList(List<T> list)
//返回由指定映射支持的同步(线程安全的)映射
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
//返回指定set支持的同步(线程安全的)set
static <T> Set<T> synchronizedSet(Set<T> s)
import java.util.*;
public class TestSynchronized
{
public static void main(String[] args)
{
//下面程序创建了四个同步的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}
9.9.4 Collesction设置不可变集合
//返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是Set,还可以是Map
emptyXxx()
//返回一个只包含指定对象(只有一个或一个元素)的不可变的集合对象,此处的集合可以是:List,Set,Map
singletonXxx():
//返回指定集合对象的不可变视图,此处的集合可以是:List,Set,Map
unmodifiableXxx():
import java.util.*;
public class TestUnmodifiable
{
public static void main(String[] args)
{
//创建一个空的、不可改变的List对象
List<String> unmodifiableList = Collections.emptyList();
//unmodifiableList.add("java");
//添加出现异常:java.lang.UnsupportedOperationException
System.out.println(unmodifiableList);// []
//创建一个只有一个元素,且不可改变的Set对象
Set unmodifiableSet = Collections.singleton("Struts2权威指南");
//[Struts2权威指南]
System.out.println(unmodifiableSet);
//创建一个普通Map对象
Map scores = new HashMap();
scores.put("语文" , 80);
scores.put("Java" , 82);
//返回普通Map对象对应的不可变版本
Map unmodifiableMap = Collections.unmodifiableMap(scores);
//下面任意一行代码都将引发UnsupportedOperationException异常
unmodifiableList.add("测试元素");
unmodifiableSet.add("测试元素");
unmodifiableMap.put("语文",90);
}
}
10.JavaSE:IO流
10.1 输入/输出流分类
一、可以从不同的角度对其进行分类,输入流和输出流都是站在程序的角度上来说
1.输入流和输出流:数据流的方向不同
2.字节流和字符流:处理数据单位不同
3.节点流和处理流:功能不同
二、字节流与字符流
1.字节流:最原始的一个流,读出来的数据就是010101这种最底层的数据表示形式,只不过它是按 照字节来读的,一个字节B是8 位bit读的时候不是一个位一个位的来读,而是一个字节 一个字节来读
2.字符流:字符流是一个字符一个字符地往外读取数据,一个字符是2个字节
三、输入流与输出流
1.输入流:InputStream(字节流),Reader(字符流)
2.输出流:OutPutStream(字节流),Writer(字符流)
四、节点流
节点流就是一根管道直接插到数据源上面,直接读数据源里面的数据,或者是直接往数据源里面写入数 据。典型的节点流是文件流:文件的字节输入流(FileInputStream),文件的字节输出流 (FileOutputStream),文件的字符输入流(FileReader),文件的字符输出流(FileWriter)
类型 | 字符流 | 字节流 |
---|---|---|
File(文件) | FileReader、FileWriter | FileInputStream、FileOutputStream |
Memory Array | CharArrayReader、 CharArrayWriter | ByteArrayInputStream、 ByteArrayOutputStream |
Memory String | StringReader、StringWriter | - |
Pipe(管道) | PipedReader、PipedWriter | PipedInputStream、PipedOutputStream |
五、处理流
处理流是包在别的流上面的流,相当于是包到别的管道上面的管道
处理类型 | 字符流 | 字节流 |
---|---|---|
Buffering | BufferedReader、 BufferedWriter | BufferedInputStream、 BufferedOutputStream |
Filtering | FilterReader、FilterWriter | FilterInputStream, FilterOutputStream |
Converting between bytes and chaacter | InputStreamReader、 OutputStreamWriter | - |
Object Serialization | - | ObjectInputStream、 ObjectOutputStream |
Data conversion | - | DataInputStream、 DataOutputStream |
Counting | LineNumberReader | LineNumberInputStream |
Peeking ahead | PusbackReader | PushbackInputStream |
Printing | PrintWriter | PrintStream |
10.2 InputStream
我们看到的具体的某一些管道,凡是以InputStream结尾的管道,都是以字节的形式向我们的程序输入数据
一、基本方法
//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
/*是一个字节一个字节地往外读,每读取一个字节,就处理一个字节*/
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
/*先把读取到的数据填满这个byte[]类型的数组buffer(buffer是内存里面的一块缓冲区),然后再处理数组里面的数据*/
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException
二、使用方法
以File(文件)这个类型作为讲解节点流的典型代表
public class TestFileInputStream {
public static void main(String args[]) {
int b = 0;// 使用变量b来装调用read()方法时返回的整数
FileInputStream in = null;
// 使用FileInputStream流来读取有中文的内容时,读出来的是乱码,因为使用InputStream流里面的read()方法读取内容时是一个字节一个字节地读取的,而一个汉字是占用两个字节的,所以读取出来的汉字无法正确显示
// 使用FileReader流来读取内容时,中英文都可以正确显示,因为Reader流里面的read()方法是一个字符一个字符地读取的,这样每次读取出来的都是一个完整的汉字,这样就可以正确显示了
try {
FileReader in = new FileInputStream("E:\\chapter\\Student.java");
} catch (FileNotFoundException e) {
System.out.println("系统找不到指定文件!");
System.exit(-1);// 系统非正常退出
}
long num = 0;// 使用变量num来记录读取到的字符数
// 调用read()方法时会抛异常,所以需要捕获异常
try {
while ((b = in.read()) != -1) {
// 调用int read() throws Exception方法时,返回的是一个int类型的整数
// 循环结束的条件就是返回一个值-1,表示此时已经读取到文件的末尾了。
// System.out.print(b+"\t");//如果没有使用“(char)b”进行转换,那么直接打印出来的b就是数字,而不是英文和中文了
System.out.print((char) b);
// “char(b)”把使用数字表示的汉字和英文字母转换成字符输入
num++;
}
in.close();// 关闭输入流
System.out.println();
System.out.println("总共读取了" + num + "个字节的文件");
} catch (IOException e1) {
System.out.println("文件读取错误!");
}
}
}
10.3 OutputStream
继承自OutputStream的流是用于程序中输出数据,且数据的单位为字节(8bit)
一、基本方法
//向输出流中写入一个字节数据,该字节数据为参数b的低8位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException
二、使用方法
使用FileOutputStream流往一个文件里面写入数据
FileInputStream和FileOutputStream这两个流都是字节流,都是以一个字节为单位进行输入和输出的,所以对于占用2个字节存储空间的字符来说读取出来时就会显示成乱码
public class TestFileOutputStream {
public static void main(String args[]) {
int b = 0;
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("E:\\src\\com\\chapter\\Student.java");
out = new FileOutputStream("E:\\src\\com\\chapter\\StudentNew.java");
// 指明要写入数据的文件,如果指定的路径中不存在StudentNew.java这样的文件,则系统会自动创建一个
while ((b = in.read()) != -1) {
out.write(b);
// 调用write(int c)方法把读取到的字符全部写入到指定文件中去
}
in.close();
out.close();
} catch (FileNotFoundException e) {
System.out.println("文件读取失败");
System.exit(-1);// 非正常退出
} catch (IOException e1) {
System.out.println("文件复制失败!");
System.exit(-1);
}
System.out.println("Student.StudentNew.java里面");
}
}
10.4 Reader流
和InputStream一模一样,唯一的区别就在于读的数据单位不同;
使用Reader流读取数据时都是两个字节两个字节往外读的,因为有些字符是占2个字节的,如我们的中文字符在Java里面就是占 两个字节的。如果采用一个字节一个字节往外读的方式,那么读出来的就是半个汉字,这样子Java就没 有办法正确的显示中文字符的,所以有必要存在这种流,一个字符一个字符地往外读
//读取一个字节并以整数的形式返回(0~255)
//如果返回-1就说明已经到了输入流的末尾
int read() throws IOException
//读取一系列字节并存储到一个数组buffer
//返回实际读取的字节数,如果读取前已到输入流的末尾,则返回-1
int read(byte[] buffer) throws IOException
//读取length个字节
//并存储到一个字节数组buffer,从length位置开始
//返回实际读取的字节数,如果读取前以到输入流的末尾返回-1.
int read(byte[] buffer,int offset,int length) throws IOException
//关闭流释放内存资源
void close() throws IOException
//跳过n个字节不读,返回实际跳过的字节数
long skip(long n) throws IOException
10.5 Writer流
继承自Writer的流都是用于程序中输出数据,且数据的单位为字符(16bit);
一、基本方法
//向输出流中写入一个字节数据,该字节数据为参数b的低16位
void write(int b) throws IOException
//将一个字节类型的数组中的数据写入输出流
void write(byte[] b) throws IOException
//将一个字节类型的数组中的从指定位置(off)开始的len个字节写入到输出流
void write(byte[] b,int off,int len) throws IOException
//关闭流释放内存资源
void close() throws IOException
//将输出流中缓冲的数据全部写出到目的地
void flush() throws IOException
二、使用方法
使用FileWriter(字符流)向指定文件中写入数据
/*使用FileWriter(字符流)向指定文件中写入数据写入数据时以1个字符为单位进行写入*/
import java.io.*;
public class TestFileWriter{
public static void main(String args[]){
/*使用FileWriter输出流从程序把数据写入到Uicode.dat文件中使用FileWriter流向文件写入数据时是一个字符一个字符写入的*/
FileWriter fw = null;
try{
fw = new FileWriter("E:src\\com\\chapter\\StudentNew.java");
//字符的本质是一个无符号的16位整数
//字符在计算机内部占用2个字节
//这里使用for循环把0~60000里面的所有整数都输出
//这里相当于是把全世界各个国家的文字都0~60000内的整数的形式来表示
for(int c=0;c<=60000;c++){
//使用write(int c)把0~60000内的整数写入到指定文件内
//调用write()方法时,我认为在执行的过程中应该使用了“(char)c”进行强制转换,即把整数转换成字符来显示
//因为打开写入数据的文件可以看到,里面显示的数据并不是0~60000内的整数,而是不同国家的文字的表示方式
fw.write(c);
}
/*使用FileReader(字符流)读取指定文件里面的内容读取内容时是以一个字符为单位进行读取的*/
int b = 0;
long num = 0;
FileReader fr = null;
fr = new FileReader("E:src\\com\\chapter\\StudentNew.java");
while((b = fr.read())!= -1){
System.out.print((char)b + "\t");
num++;
}
System.out.println("总共读取了"+num+"个字符");
}catch(Exception e){
e.printStackTrace();
}
}
}
10.6 处理流
10.6.1 缓冲流Buffering
一、缓冲流也包含了四个类:BufferedInputStream、 BufferedOutputStream、BufferedReader和BufferedWriter
二、流都是成对的,没有流是是不成对的, 肯定是一个in,一个out
三、缓冲流要”套接“在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,同时增加 了一些新的方法;J2SDK提供了四种缓冲流,常用构造方法如下:
1.缓冲输入流支持其父类的mark和reset方法
2.BufferedReader提供了readLine方法用于读取一行字符串
3.BufferedWriter提供了newLine用于写入一个行分隔符
4.对于输出的缓冲流,写出的数据会现在内存中缓存,使用flush方法将会使内存中的数据立刻写出带有缓冲区的
BufferedReader(Reader in)
BufferedReader(Reader in,int sz) //sz为自定义缓冲区的大小
BufferedWriter(Writer out)
BufferedWriter(Writer out,int sz)
BufferedInputStream(InputStream in)
BufferedInputStream(InputStream in,int size)
BufferedOutputStream(InputStream in)
BufferedOutputStream(InputStream in,int size)
//BufferedInputStream
import java.io.*;
public class TestBufferStream {
public static void main(String args[]) {
FileInputStream fis = null;
try {
fis = new FileInputStream("E:\\src\\com\\chapter\\Student.java");
// 在FileInputStream节点流的外面套接一层处理流BufferedInputStream
BufferedInputStream bis = new BufferedInputStream(fis);
int c = 0;
System.out.println((char) bis.read());
System.out.println((char) bis.read());
bis.mark(100);// 在第100个字符处做一个标记
for (int i = 0; i <= 10 && (c = bis.read()) != -1; i++) {
System.out.print((char) c);
}
bis.reset();// 重新回到原来标记的地方
for (int i = 0; i <= 10 && (c = bis.read()) != -1; i++) {
System.out.print((char) c);
}
bis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
//BufferedReader
public class TestBufferStream{
public static void main(String args[]){
try{
BufferedWriter bw = new BufferedWriter(new FileWriter("E:src\\com\\chapter\\Student.txt"));
//在节点流FileWriter的外面再套一层处理流BufferedWriter
String s = null;
for(int i=0;i<100;i++){
s = String.valueOf(Math.random());//“Math.random()”将会生成一系列介于0~1之间的随机数。
// static String valueOf(double d)这个valueOf()方法的作用就是把一个double类型的数转换成字符串
//valueOf()是一个静态方法,所以可以使用“类型.静态方法名”的形式来调用
bw.write(s);//把随机数字符串写入到指定文件中
bw.newLine();//调用newLine()方法使得每写入一个随机数就换行显示
}
bw.flush();//调用flush()方法清空缓冲区
BufferedReader br = new BufferedReader(new FileReader("E:src\\com\\chapter\\Student.txt"));
//在节点流FileReader的外面再套一层处理流BufferedReader
while((s = br.readLine())!=null){
//使用BufferedReader处理流里面提供String readLine()方法读取文件中的数据时是一行一行读取的
//循环结束的条件就是使用readLine()方法读取数据返回的字符串为空值后则表示已经读取到文件的末尾了。
System.out.println(s);
}
bw.close();
br.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
10.6.2 字符转换流
1.转换流非常的有用,它可以把一个字节流转换成一个字符流
2.转换流有两种,一种叫InputStreamReader,另一种叫OutputStreamWriter
3.InputStream是字节流,Reader是字符流, InputStreamReader就是把InputStream转换成Reader
4.OutputStream是字节流,Writer是字符流, OutputStreamWriter就是把OutputStream转换成Writer
5.InputStreamReader 和 OutputStreamWriter 用于字节数据到字符数据之间的转换
6.InputStreamReader 需要和 InputStream “套接”
7.OutputStreamWriter 需要和 OutputStream “套接”
8.转换流在构造时可以指定其编码集合
9.把OutputStream转换成Writer之后就可以 一个字符一个字符地通过管道写入数据了,而且还可以写入字符串
10.如果用一个FileOutputStream 流往文件里面写东西,得要一个字节一个字节地写进去,但是如果在FileOutputStream流上面套 上一个字符转换流,那我们就可以一个字符串一个字符串地写进去
//转换流测试
public class TestTransform1 {
public static void main(String args[]) {
try {
OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("D:/java/char.txt"));
osw.write("MircosoftsunIBMOracleApplet");// 把字符串写入到指定的文件中去
System.out.println(osw.getEncoding());// 使用getEncoding()方法取得当前系统的默认字符编码
osw.close();
osw = new OutputStreamWriter(new FileOutputStream("D:\\java\\char.txt", true), "ISO8859_1");
// 如果在调用FileOutputStream的构造方法时没有加入true,那么新加入的字符串就会替换掉原来写入的字符串,在调用构造方法时指定了字符的编码
osw.write("MircosoftsunIBMOracleApplet");// 再次向指定的文件写入字符串,新写入的字符串加入到原来字符串后面
System.out.println(osw.getEncoding());
osw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
10.6.3 数据流
1.DataInputStream 和 DataOutputStream分别继承自InputStream和OutputStream , 它属于处理流,需要分别“套接”InputStream 和 OutputStream类型的节点流上
2.DataInputStream 和 DataOutputStream 提供了可以存取与机器无关的Java原始类型数据(int、double等)的方法
3.DataInputStream 和 DataOutputStream 的构造方法
DataInputStream (InputStream in)
DataOutputStream (OutputStream out)
//数据流测试
import java.io.*;
public class TestDataStream{
public static void main(String args[]){
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//在调用构造方法时,首先会在内存里面创建一个ByteArray字节数组
DataOutputStream dos = new DataOutputStream(baos);
//在输出流的外面套上一层数据流,用来处理int,double类型的数
try{
dos.writeDouble(Math.random());//把产生的随机数直接写入到字节数组ByteArray中
dos.writeBoolean(true);//布尔类型的数据在内存中就只占一个字节
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
System.out.println(bais.available());
DataInputStream dis = new DataInputStream(bais);
System.out.println(dis.readDouble());//先写进去的就先读出来,调用readDouble()方法读取出写入的随机数
System.out.println(dis.readBoolean());//后写进去的就后读出来,这里面的读取顺序不能更改位置,否则会打印出不正确的结果
dos.close();
bais.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
/*
通过bais这个流往外读取数据的时候,是一个字节一个字节地往外读取的,因此读出来的数据无法判断是字符串还是bool类型的值,因此要在它的外面再套一个流,通过dataInputStream把读出来的数据转换就可以判断了。注意了:读取数据的时候是先写进去的就先读出来,因此读ByteArray字节数组数据的顺序应该是先把占8个字节的double类型的数读出来,然后再读那个只占一个字节的boolean类型的数,因为double类型的数是先写进数组里面的,读的时候也要先读它。这就是所谓的先写的要先读。如果先读Boolean类型的那个数,那么读出来的情况可能就是把double类型数的8个字节里面的一个字节读了出来
*/
10.7 打印流
1.PrintWriter 和 PrintStream 都属于输出流,分别针对与字符和字节
2.PrintWriter 和 PrintStream 提供了重载的print
3.Println方法用于多种数据类型的输出
4.PrintWriter和PrintStream的输出操作不会抛出异常,用户通过检测错误状态获取错误信息
5.PrintWriter 和 PrintStream有自动flush功能
//构造方法
PrintWriter(Writer out)
PrintWriter(Writer out,boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out,boolean autoFlush)
PrintStream(OutputStream out)
PrintStream(OutputStream out,boolean autoFlush)
/*这个小程序是重新设置打印输出的窗口,
* 把默认在命令行窗口输出打印内容设置成其他指定的打印显示窗口
*/
import java.io.*;
public class TestPrintStream{
public static void main(String args[]){
PrintStream ps = null;
try{
FileOutputStream fos = new FileOutputStream("D:\\src\\com\\chapter\\log.txt");
ps = new PrintStream(fos);//在输出流的外面套接一层打印流,用来控制打印输出
if(ps != null){
System.setOut(ps);
//这里调用setOut()方法改变了输出窗口,以前写System.out.print()默认的输出窗口就是命令行窗口
//但现在使用System.setOut(ps)将打印输出窗口改成了由ps指定的文件里面,通过这样设置以后,打印输出时都会在指定的文件内打印输出
//在这里将打印输出窗口设置到了log.txt这个文件里面,所以打印出来的内容会在log.txt这个文件里面看到
}
for(char c=0;c<=1000;c++){
System.out.print(c+"\t");//把世界各国的文字打印到log.txt这个文件中去
}
}catch(Exception e){
e.printStackTrace();
}
}
}
10.7 对象流
直接将Object 写入或读出
一、transient关键字
透明的,用它来修饰的成员变量在序列化的时候不予考虑,也就是当成不存在;用transient关键字标记的成员变量不参与序列 化过程
二、serializable接口
现该序列化接口,表明该类可以被序列化,什么是序列化?简单的说,就是能够从类变成字节流传输,然后还能从字节流变成原 来的类
三、externaliazble接口
继承了serializable接口,该接口中定义了两个抽象方法:writeExternal()与readExternal();当使用Externalizable接口来进行序 列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法
import java.io.*;
public class TestObjectIo {
public static void main(String args[]) {
T t = new T();
t.k = 8;//把k的值修改为8
try {
FileOutputStream fos = new FileOutputStream("E:\\chapter\\TestObjectIo.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
// ObjectOutputStream流专门用来处理Object的,在fos流的外面套接ObjectOutputStream流就可以直接把一个Object写进去
oos.writeObject(t);// 直接把一个t对象写入到指定的文件里面
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("E:chapter\\TestObjectIo.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
// ObjectInputStream专门用来读一个Object的
T tRead = (T) ois.readObject();
// 直接把文件里面的内容全部读取出来然后分解成一个Object对象,并使用强制转换成指定类型T
System.out.print(tRead.i + "\t" + tRead.j + "\t" + tRead.d +"\t"+ tRead.k);
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
* 凡是要将一个类的对象序列化成一个字节流就必须实现Serializable接口
* Serializable接口中没有定义方法,Serializable接口是一个标记性接口,用来给类作标记,只是起到一个标记作用。
* 这个标记是给编译器看的,编译器看到这个标记之后就可以知道这个类可以被序列化 如果想把某个类的对象序列化,就必须得实Serializable接口
*/
class T implements Serializable {
// Serializable的意思是可以被序列化的
int i = 10;
int j = 9;
double d = 2.3;
int k = 15;
// transient int k = 15;
// 在声明变量时如果加上transient关键字,那么这个变量就会被当作是透明的,即不存在。
}
11.JavaSE:多线程
11.1 线程创建的三种方式
一、Thread类
1.自定义线程类继承Thread类
2.重写run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程
public static void main(String args[]){
StartThread thread = new StartThread();
//线程不一定立即执行,CPU安排调度
thread.start();
}
//线程入口处
public class StartThraed extends Thread{
@override
public void run(){
//重写线程体
for(int i=10;i<20;i++){
System.out.println("线程执行了")
}
}
}
二、Runnable接口
1.定义MyRunnable类实现Runnable接口
2.实现run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程
public class StartRunnable implements Runnable{
@overrride
public void run(){
//重写线程体
for(int i=10;i<20;i++){
System.out.println("线程执行了")
}
}
}
//创建实现类对象
StartRunnable startrunnable = new StartRunnable();
//创建代理类对象
Thread thread = new Thread(startrunnable);
//启动
thread.start();
三、Callable接口(了解即可)
1.实现Callable接口,需要返回值类型
2.重写call方法,需要抛出异常
3.创建目标对象
4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
5.提交执行:Future result1 = ser.submit(t1);
6.获取结果:boolean r1 = result1.get()
7.关闭服务:ser.shutdownNow();
四、总结
1.继承Thread类
子类继承Thread类具备多线程能力
启动线程:子类对象. start()
不建议使用:避免OOP单继承局限性
2.实现Runnable接口
实现接口Runnable具有多线程能力
启动线程:传入目标对象+Thread对象.start()
推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
//一份资源
StartThread startthread = new StartThread();
//多个代理
new Thread(staion,"XiaoGuo").start();
new Thread(staion,"XiaoLiu").start();
new Thread(staion,"XiaoWang").start();
11.2 线程状态
创建状态、就绪状态、阻塞状态、运行状态、死亡状态
//线程对象一旦创建就进入到了新生状态
Thread thread = new Thread()
//调用当调用start()方法,线程立即进入就绪状态,但不意味着立即调度执行
thread.start();
//当调用sleep,wait或同步锁定时,线程进入阻塞状态,阻塞事件解除后,重新进入就绪状态,等待cpu调度执行
thread.wait();
thread.sleep();
//进入运行状态,线程才真正执行线程体的代码块
//thread run ....
//线程中断或者结束,一旦进入死亡状态,就不能再次启动
//thread deep ....
//尚未启动的线程处于此状态
NEW
//在JVM中执行的线程处于此状态
RUNNABLE
//被阻塞等待监视器锁定的线程处于此状态
BLOCKED
//正在等待另一个线程执行特定动作的线程处于此状态
WAITING
//正在等待另一个线程执行特定动作达到等待时间的线程处于此状态
TIME_WAITING
//已退出的线程处于此状态
TERMINATED
11.3 线程方法
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程,别用这个方式 |
boolean isAlive() | 测试线程是否处于活动状态 |
11.3.1 停止线程
1.【已废弃】不推荐使用JDK提供的 stop()、destroy()方法
2.推荐线程自己停止下来
3.建议使用一个标志位进行终止变量当flag=false,则终止线程运行
public class TestStop implements Runnable{
//1.线程中定义线程体使用的标识
private boolean flag = true;
@override
public void run(){
//2.线程体使用该标识
while(flag){
System.out.println("run....Thread");
}
}
//3.对外提供方法改变标识
public void stop(){
this.flag = fasle;
}
}
11.3.2 线程休眠
1.sleep (时间) 指定当前线程阻塞的毫秒数
2.sleep存在异常InterruptedException
3.sleep时间达到后线程进入就绪状态
4.sleep可以模拟网络延时,倒计时等
5.每一个对象都有一个锁,sleep不会释放锁
Public void GetOnlineInfo(){
Thread thread = new Thread();
thread.sleep(2000);
}
11.3.3 线程礼让
1.礼让线程,让当前正在执行的线程暂停,但不阻塞
2.将线程从运行状态转为就绪状态
3.让cpu重新调度,礼让不一定成功!看CPU心情
// b--->Strat
// a--->Strat
// b--->End
// a--->End
11.3.4 Join
1.Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
2.可以想象成插队
public class TestJoin implements Runnable{
public static void main(String[] args) throws InterruptedException{
TestJoin testjoin = new TestJoin();
Thred thread = new Thread(testJoin);
thread.start();
for(int i = 0;i<100;i++){
if(i==50){
//main线程阻塞
thread.join();
}
System.out.println("main"+i);
}
}
@override
public void run(){
for(int i = 0 ; i<1000;i++){
System.out.println("join"+i);
}
}
}
11.4 线程优先级
一、Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
二、线程的优先级用数字表示,范围从1~10.
//优先级的设定,建议在start()调度之前
//优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;
//使用用以下方式改变或获取优先级
getPriority().setPriority(int xxx)
11.5 守护线程
1.线程分为用户线程和守护线程
2.虚拟机必须确保用户线程执行完毕
3.虚拟机不用等待守护线程执行完毕,如后台记录操作日志,监控内存,垃圾回收等待
11.6 线程同步
一、由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入锁机制synchronized ,
二、当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 ,使用后释放锁即可 . 存在以下问题
1.一个线程持有锁会导致其他所有需要此锁的线程挂起
2.在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换和调度延时,引起性能问题
3.如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置 , 引起性能问题
11.7 同步方法
一、由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需要针对方法提出一套机制
二、这套机制就是 synchronized 关键字 , 它包括两种用法 :synchronized 方法 和synchronized 块
三、synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞
四、方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获得这个锁 , 继续执行
//同步方法
public synchronized void method(int args) {
//缺点:如果将一个大的方法声明为synchronized,将会影响效率
}
//同步块
public synchronized (Obj) {
/*
Obj称之为同步监视器
Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中讲解]
*/
}
******************************************************************
//同步监视器的执行过程
//1.第一个线程访问,锁定同步监视器,执行其中代码
//2.第二个线程访问,发现同步监视器被锁定,无法访问
//3.第一个线程访问完毕,解锁同步监视器
//4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
11.8 锁
11.8.1 死锁
一、多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形,某一个同步块同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题
二、产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3.不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺
4.循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系
11.8.2 Lock
一、从JDK 5.0开始,Java提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步
二、同步锁使用Lock对象充当
三、java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
四、锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
五、ReentrantLock类实现了Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
class A{
//1.获得ReentrantLock类对象
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//2.保证线程安全的代码;
}
finally{
//3.如果同步代码有异常,要将unlock()写入finally语句块
lock.unlock();
}
}
}
六、synchronized 与 Lock的对比
1.Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)
2.synchronized是隐式锁,出了作用域自动释放
3.Lock只有代码块锁,synchronized有代码块锁和方法锁
4.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好;并且具有更好的扩展性(提供更多的子类)
5.优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)
11.9 线程池
一、使用背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
二、思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中;可以避免频繁创建销毁、实现重复利用;类似生活中的公共交通工具
三、 好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建,便于线程管理)
3.corePoolSize:核心池的大小
4.maximumPoolSize:最大线程数
5.keepAliveTime:线程没有任务时最多保持多长时间后会终止
四、线程池的使用:
1.JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
2.ExecutorService:真正的线程池接口;常见子类ThreadPoolExecutor
3.Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
//执行任务/命令,没有返回值,一般用来执行Runnable
void execute(Runnable command)
//执行任务,有返回值,一般又来执行Callable
<T> Future<T> submit(Callable<T> task)
//关闭连接池
void shutdown()