关于多线程,首先需要理解三个基本概念:
-
什么是程序?
程序是为完成特定任务、用某种语言编写的一组指令的集合。
-
什么是进程?
进程是程序的一次执行过程。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
什么是线程?
进程可进一步细化为线程,是一个程序内部的一条执行路径。线程是调度和执行的单位。
每个线程拥独立的运行栈和程序计数器(pc);多个线程,共享同一个进程中的方法区、堆。
线程切换的开销小,进程切换开销大,一个进程中可以有多个线程。
-
单核CPU与多核CPU的理解
- 单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)。
- 一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
-
并行与并发的理解
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
-
继承Thread类
-
实现Runnable接口
-
JDK5新增-实现Callable接口
//1.创建一个实现Callable的实现类 class NumThread implements Callable{ //2.实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { if(i % 2 == 0){ System.out.println(i); sum += i; } } return sum; } } public class ThreadNew { public static void main(String[] args) { //3.创建Callable接口实现类的对象 NumThread numThread = new NumThread(); //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(numThread); //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() new Thread(futureTask).start(); try { //6.获取Callable中call方法的返回值 //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。 Object sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }相较于实现Runnable接口,实现Callable接口的方式更加强大,具体体现在:
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
-
JDK5新增-使用线程池
class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 != 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } public class ThreadPool { public static void main(String[] args) { //1. 提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //设置线程池的属性 // System.out.println(service.getClass()); // service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象 service.execute(new NumberThread());//适合适用于Runnable service.execute(new NumberThread1());//适合适用于Runnable // service.submit(Callable callable);//适合使用于Callable //3.关闭连接池 service.shutdown(); } }线程池的优点:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没任务时最多保持多长时间后会终止
-
start():启动当前线程;调用当前线程的run()。
-
run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
-
currentThread():静态方法,返回执行当前代码的线程。
-
getName():获取当前线程的名字。
-
setName():设置当前线程的名字。
-
yield():释放当前cpu的执行权,不会释放锁。
-
join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
-
stop():已过时。当执行此方法时,强制结束当前线程。
-
sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
-
isAlive():判断当前线程是否存活。
-
getPriority():获取线程的优先级。
-
setPriority(int p):设置线程的优先级。
线程的优先级:
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->默认优先级
- 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
-
背景
例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式
-
问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
-
问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
-
如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
-
-
Java解决方案:同步机制
-
同步代码块
synchronized(同步监视器){ //需要被同步的代码 } -
同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们可以将此方法声明同步的。
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this。
静态的同步方法,同步监视器是:当前类本身。
-
Lock锁 — JDK5.0新增
synchronized 与 Lock的异同?
- 相同:二者都可以解决线程安全问题
- 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())。
-
使用的优先顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)
线程安全的单例模式:
public class SingleInstance {
private volatile static SingleInstance uniqueInstance = null;
public static SingleInstance getInstance() {
if (uniqueInstance == null) {
synchronized (SingleInstance.class) {
if (uniqueInstance == null) {
uniqueInstance = new SingleInstance();
}
}
}
return uniqueInstance;
}
}
避免出现死锁
死锁,即不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。如下:
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
2.1.5 线程通信
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
备注:
-
wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
-
wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常。
-
wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
-
sleep() 和 wait()的异同?
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
-
不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
-
- yield 和 sleep 的异同
1)yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分。
2)yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源。
3)yield 不能被中断,而 sleep 则可以接受中断。
-
问题:yield()不会释放锁,只释放cpu的执行权,有啥意义,不是只有拿到锁的线程才能执行么?
答:谁告诉你只有一把锁了???共用一把锁的其他线程不能执行,但是用别的锁的线程可以执行!
- String声明为final的,不可被继承
- String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小。
- String内部定义了final char[] value用于存储字符串数据。
- 通过字面量的方式(区别于new给一个字符串赋值,此时的字符串值声明在字符串常量池中)。
- 字符串常量池中是不会存储相同内容(使用String类的equals()比较,返回true)的字符串的。
String的不可变性
- 当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
- 当对现的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
String类实例化方式
-
通过字面量定义的方式
此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中。
String s1 = "javaEE"; String s2 = "javaEE";
-
通过new + 构造器的方式
通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。
String s3 = new String("javaEE"); String s4 = new String("javaEE"); System.out.println(s1 == s2);//true System.out.println(s1 == s3);//false System.out.println(s1 == s4);//false System.out.println(s3 == s4);//false
问题:String s = new String("abc");方式创建对象,在内存中创建了几个对象?
两个;一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:“abc”。
字符串拼接方式赋值的对比
- 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
- 只要其中一个是变量,结果就在堆中。
- 如果拼接的结果调用intern()方法,返回值就在常量池中
String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE" + "hadoop"; String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);//true System.out.println(s3 == s5);//false System.out.println(s3 == s6);//false System.out.println(s3 == s7);//false System.out.println(s5 == s6);//false System.out.println(s5 == s7);//false System.out.println(s6 == s7);//false String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop” System.out.println(s3 == s8);//true **************************** String s1 = "javaEEhadoop"; String s2 = "javaEE"; String s3 = s2 + "hadoop"; System.out.println(s1 == s3);//false final String s4 = "javaEE";//s4:常量 String s5 = s4 + "hadoop"; System.out.println(s1 == s5);//true
JVM中字符串常量池存放位置
- jdk 1.6 (jdk 6.0 ,java 6.0):方法区(永久区)
- jdk 1.7:堆空间
- jdk 1.8:方法区(元空间)
StringBuffer
初始化为一个长度为16的char[],扩容策略:2*原容量+2
2.2.2 时间API 2.2.2.1 获取系统当前时间 想获取系统当前时间,System类中的静态方法currentTimeMillis()想必大家都不会陌生,它返回的是当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差,也被称为时间戳。
- java.util.Date类与java.sql.Date类
关于这两个类,很多人可能不是特别清楚两者之间的区别与联系,接下来我们对此进行简单的梳理。
- 区别:
首先,从包名可以看出java.sql.Date类是与数据库相关的类,更准确地来说,它与数据库中的日期类型变量相对应。而java.util.Date类,就是我们平时使用的类了(事实上开发中也很少会用它,具体后面再说)。
那么除了根据包名能判断出的这些外,两者之间有什么联系呢?
- 联系:
通过查阅jdk源码可知:java.sql.Date继承了java.util.Date类,也就是说,java.sql.Date类是java.util.Date类的子类。
-
搞清楚了两者之间的区别和联系后,我们来回答两个问题:
-
第一个问题:如何将java.sql.Date对象转换为java.util.Date对象呢?
熟悉面向对象的读者肯定知道,子类对象可直接向上转型成父类对象(多态性)。据此,我们可以直接将java.sql.Date类的对象赋给java.util.Date对象,代码如下:
java.util.Date date = new java.sql.Date(136513463156L); //向java.sql.Date的构造器中传入时间戳
-
第二个问题:如何将java.util.Date对象转换为java.sql.Date对象呢?
进行强制类型转换?当然不可行。事实上,如果我们将父类对象强制转化为子类对象,将会抛出Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to java.sql.Date异常。那么就只能从两者之间的联系下手,代码如下:
java.util.Date date = new java.util.Date();// 创建java.util.Date对象 java.sql.Date sqlDate = new java.sql.Date(date.getTime());//getTime()方法会返回当前Date对象对应的毫秒数。(时间戳)
-
补充说明(了解即可):
- java.text.SimpleDataFormat类:主要用来格式化日期时间。简单介绍一下用法:
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//格式化
String format = sdf.format(date);
System.out.println(format);//2019-02-18 11:48:27
//解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现),
//否则,抛异常
Date date1 = sdf1.parse("2020-02-18 11:48:27");
System.out.println(date1);
-
Calendar类:该类是日历类,也是一个抽象类。简单介绍一下用法:
//1.创建Calendar类的对象有两种方式: //方式一:创建其子类(GregorianCalendar的对象) //方式二:调用其静态方法getInstance() Calendar calendar = Calendar.getInstance(); //2.常用方法 //get() System.out.println(calendar.get(Calendar.DAY_OF_MONTH));//获取本月内的第几天,从1开始 System.out.println(calendar.get(Calendar.DAY_OF_YEAR));//获取一年内的第几天,从1开始 //set() //calendar可变性 calendar.set(Calendar.DAY_OF_MONTH,22); // 设置日期为本月内的第22天 System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); //输出22 //add() calendar.add(Calendar.DAY_OF_MONTH,-3); // 设置日期为今天的日期-3天; System.out.println(calendar.get(Calendar.DAY_OF_MONTH)); // 今天是11月8号,所以输出5 //getTime():日历类---> Date Date date = calendar.getTime(); // 返回Date对象 System.out.println(date);//输出Sun Nov 08 17:49:57 CST 2020 //setTime():Date ---> 日历类 Date date = new Date(); calendar.setTime(date); System.out.println(calendar.get(Calendar.YEAR)); // 输出2020 System.out.println(date.getYear()); // 输出120,此方法输出的是今年与1900年之差
Calendar类本意是为了弥补了Date类的缺陷而生的,但是随着在开发中的使用,渐渐的暴露出它们的不足。比如糟糕的可变性、偏移性等等。下面,我们一起来看看jdk1.8之后,关于日期的处理。
回顾Java对日期时间的迭代:
- 第一代:jdk 1.0 Date类
- 第二代:jdk 1.1 Calendar类,一定程度上替换Date类
- 第三代:jdk 1.8 提出了新的一套API
前两代存在的问题举例:
- 可变性:像日期和时间这样的类应该是不可变的。
- 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
- 格式化:格式化只对Date用,Calendar则不行。
- 此外,它们也不是线程安全的;不能处理闰秒等。
java 8 中新的日期时间API涉及到的包:
- 本地日期、本地时间、本地日期时间的使用:LocalDate / LocalTime / LocalDateTime
-
简介
① 分别表示使用 ISO-8601日历系统的日期、时间、日期时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
② LocalDateTime相较于LocalDate、LocalTime,使用频率要高
③ 类似于Calendar -
常用方法:
- 时间点:Instant
-
简介
① 时间线上的一个瞬时点。 概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC开始的秒数。)
② 类似于 java.util.Date类 -
常用方法:
- 日期时间格式化类:DateTimeFormatter
-
简介
① 格式化或解析日期、时间
② 类似于SimpleDateFormat -
实例化方式:
- 预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
- 本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
- 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
-
常用方法:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); //格式化 String str = formatter.format(LocalDateTime.now()); System.out.println(str);//2020-11-08 06:48:13 //解析 TemporalAccessor accessor = formatter.parse("2020-11-08 06:48:13"); System.out.println(accessor); //输出{SecondOfMinute=13, MinuteOfHour=48, MicroOfSecond=0, MilliOfSecond=0, NanoOfSecond=0, HourOfAmPm=6},ISO resolved to 2020-11-08
- 其他API
-
带时区的日期时间:ZonedDateTime / ZoneId
//获取“Asia/Tokyo”时区对应的时间 LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo")); System.out.println(localDateTime);//2020-11-08T19:57:38.479 //now()/now(ZoneId id):获取当前时区/指定时区的ZonedDateTime对象 ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo")); System.out.println(zonedDateTime);//2020-11-08T19:58:45.084+09:00[Asia/Tokyo] -
时间间隔:Duration–用于计算两个“时间”间隔,以秒和纳秒为基准
//设置起始日期时间,获取当前日期时间 LocalDateTime localDateTime = LocalDateTime.of(2016, 11, 8, 19, 2, 00); LocalDateTime localDateTime1 = LocalDateTime.now(); Duration duration = Duration.between(localDateTime, localDateTime1);//返回间隔对象 System.out.println(duration.toDays()); //输出1461
-
日期间隔:Period --用于计算两个“日期”间隔,以年、月、日衡量
LocalDate localDate = LocalDate.now(); LocalDate localDate1 = LocalDate.of(2030, 11, 8); Period period = Period.between(localDate, localDate1); System.out.println(period); //P10Y System.out.println(period.getYears()); // 10 System.out.println(period.getMonths()); // 0 System.out.println(period.getDays()); // 0
-
日期时间校正器:TemporalAdjuster
//获取当前日期的下一个周日是哪天? TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY); LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster); System.out.println(localDateTime); //2020-11-15T19:15:51.426 //获取下一个工作日是哪天?今天是2020-11-08 LocalDate localDate = LocalDate.now().with(new TemporalAdjuster(){ @Override public Temporal adjustInto(Temporal temporal) { LocalDate date = (LocalDate)temporal; if(date.getDayOfWeek().equals(DayOfWeek.FRIDAY)){ return date.plusDays(3);//如果是周五,则+3 }else if(date.getDayOfWeek().equals(DayOfWeek.SATURDAY)){ return date.plusDays(2);//如果是周六,则+2 }else{ return date.plusDays(1);//其他情况,则+1 } } }); System.out.println("下一个工作日是:" + localDate); //下一个工作日是:2020-11-09
- 引子
在比较Java对象时,正常情况下,我们只能使用==或!=进行比较,而不能使用>或者<这样的运算符。
那么问题来了,开发过程中我们经常要对多个对象排序,排序必然需要比较,那么如何实现呢?
其实实现也很简单,使用Comparable或者Comparator两个接口中的一个即可。
- Comparable接口的使用
先来看一段代码:
public static void main(String[] args) {
String[] arr = new String[] {"dd","aa","xx","cc"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // 输出[aa, cc, dd, xx]
}
执行上面的代码,输出结果为:[aa, cc, dd, xx]。
为什么呢?查阅String类的源码可知,String类实现了Comparable接口的compareTo(obj)方法。
在重写compareTo(obj)方法时,我们需要满足三个规则:
- 如果当前对象this大于形参对象obj,则返回正整数,
- 如果当前对象this小于形参对象obj,则返回负整数,
- 如果当前对象this等于形参对象obj,则返回零。
那么让我们自定义的类实现比较就很简单了,只需要按照上面三个规则实现Comparable接口中的compareTo(obj)方法,再调用Arrays.sort(arr)或者Collections.sort(list)即可。
那么什么情况下都能够以实现Comparable接口的方式来定义对象的比较方式吗?
当然不是!实际上,虽然Comparable接口可以解决一部分业务需求,但是当我们无法使某个类实现Comparable接口时(比如第三方Jar包中的类),我们通常会使用另一种方式—实现Comparator接口。
- Comparator接口的使用
与实现Comparable接口类似,使用Comparator接口同样需要我们实现一个方法—compare(obj1,obj2)。
在重写compare(Object o1,Object o2)方法时,我们同样需要满足三个规则:
- 如果方法返回正整数,则表示o1大于o2;
- 如果返回0,表示相等;
- 返回负整数,表示o1小于o2。
示例代码如下:
public static void main(String[] args) {
Comparator comparator = (o1, o2) -> {
if (o1 instanceof Person && o2 instanceof Person) {
Person preson1 = (Person) o1;
Person preson2 = (Person) o1;
return Integer.compare(preson1.getAge(),preson2.getAge());
}
throw new RuntimeException("输入的数据类型不一致");
};
Person p1 = new Person();
p1.setAge(18);
Person p2 = new Person();
p2.setAge(20);
Person[] people = new Person[]{p1, p2};
Arrays.sort(people,comparator);
System.out.println(Arrays.toString(people)); // 输出[Person{age=18}, Person{age=20}]
}
2.2.4 枚举类
-
使用
//使用enum关键字枚举类 enum Season1 { //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束 SPRING("春天","春暖花开"), SUMMER("夏天","夏日炎炎"), AUTUMN("秋天","秋高气爽"), WINTER("冬天","冰天雪地"); //2.声明Season对象的属性:private final修饰 private final String seasonName; private final String seasonDesc; //2.私化类的构造器,并给对象属性赋值 private Season1(String seasonName,String seasonDesc){ this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //4.其他诉求1:获取枚举类对象的属性 public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } } -
常用方法
Season1 summer = Season1.SUMMER; //toString():返回枚举类对象的名称 System.out.println(summer.toString()); //System.out.println(Season1.class.getSuperclass()); System.out.println("****************"); //values():返回所的枚举类对象构成的数组 Season1[] values = Season1.values(); for(int i = 0;i < values.length;i++){ System.out.println(values[i]); } System.out.println("****************"); Thread.State[] values1 = Thread.State.values(); for (int i = 0; i < values1.length; i++) { System.out.println(values1[i]); } //valueOf(String objName):返回枚举类中对象名是objName的对象。 Season1 winter = Season1.valueOf("WINTER"); //如果没objName的枚举类对象,则抛异常:IllegalArgumentException // Season1 winter = Season1.valueOf("WINTER1"); System.out.println(winter); -
枚举类对象分别实现接口
interface Info{ void show(); } //使用enum关键字枚举类 enum Season1 implements Info{ //1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束 SPRING("春天","春暖花开"){ @Override public void show() { System.out.println("春天在哪里?"); } }, SUMMER("夏天","夏日炎炎"){ @Override public void show() { System.out.println("宁夏"); } }, AUTUMN("秋天","秋高气爽"){ @Override public void show() { System.out.println("秋天不回来"); } }, WINTER("冬天","冰天雪地"){ @Override public void show() { System.out.println("大约在冬季"); } }; }
注解是什么?
Java注解是附加在代码中的一些元信息 ,简单来说,注解就算一个辅助工具,可以让我们的代码更加简洁等。
为什么要学习注解
首先,我们平时使用的框架,就是靠着注解才得以使我们开发更加方便。框架=注解+反射+设计模式
通过注解你可以将你的某个方法以注解的形式调用,就像Spring中的@Controller、@Component等注解一样。
首先说明一些概念:
-
什么是元注解?
元注解就是对现有注解进行说明的注解。
为此,我们接下来来了解一下JDK5.0提供的四个元注解:
-
@Retention:指定所修饰的 Annotation 的生命周期。参数:
-
RetentionPolicy.SOURCE—注解只在源文件中有效
-
RetentionPolicy.CLASS—注解在class文件中有效,当运行Java程序时,该注解不会被保留。默认值。
-
RetentionPolicy.RUNTIME—运行时有效。程序可以通过反射来获取该注解。
-
-
@Target:用于指定被修饰的 注解能用于修饰哪些程序元素
-
@documented:表示所修饰的注解在被javadoc解析时,保留下来。
-
@Inherited:被它修饰的注解将具继承性。
怎么使用注解
了解了上面的概念后,我们可以来实现一个简单的注解,代码如下:
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效,即可通过反射获取
@Target({ElementType.METHOD,ElementType.PARAMETER}) // 注解可用在方法和参数上。
@Inherited // 注解可继承
@documented // javadoc解析时保留
@interface HelloAnnotation {
String value() default "hello";
}
@HelloAnnotation
public void setAge() {
}
注解只是提供一个辅助信息,要跟反射结合在一起使用才可以实现某项功能!
补充知识:Java8中注解的新特性
-
可重复注解:(当需要在一个方法上使用多次注解时使用)
① 在HelloAnnotation上声明@Repeatable,成员值为HelloAnnotations.class
② HelloAnnotation的Target和Retention等元注解与HelloAnnotations相同。
代码示例如下:
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效,即可通过反射获取 @Target(ElementType.METHOD) // 注解可用在方法上 @Inherited // 注解可继承 @Repeatable(HelloAnnotations.class)//可重复 @documented // javadoc解析时保留 @interface HelloAnnotation { String value() default "hello"; } @Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效,即可通过反射获取 @Target(ElementType.METHOD) // 注解可用在方法上 @Inherited // 注解可继承 @documented // javadoc解析时保留 @interface HelloAnnotations { HelloAnnotation value()[]; } //使用如下: @HelloAnnotation @HelloAnnotation public static void main(String[] args) { } -
类型注解:
①ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中,如:泛型声明。示例代码如下:
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效,即可通过反射获取 @Target({ElementType.TYPE_PARAMETER }) // 注解可用在方法上 @interface HelloAnnotation { String value() default "hello"; } class TestAnnotation<@HelloAnnotation T> { }②ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中。
示例代码如下:
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效,即可通过反射获取 @Target({ElementType.TYPE_USE}) // 注解可用在方法上 @interface HelloAnnotation { String value() default "hello"; } public static void main(String[] args) throws @HelloAnnotation RuntimeException{ @HelloAnnotation int i = 0; List<@HelloAnnotation String> list = null; }
|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的、可重复的数据。 -->“动态”数组
|----ArrayList、linkedList、Vector
|----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”
|----HashSet、linkedHashSet、TreeSet
向Collection接口的实现类的对象中添加对象obj时,要求obj所在类要重写equals()。
遍历Collection的两种方式:
- 使用迭代器Iterator
- foreach循环(或增强for循环)
使用集合的remove()方法时需要注意:
如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。因为“指针”还没有下移。
2.3.2 Collection子接口:List接口List存储有序的、可重复的数据。
2.3.2.1 List接口常用实现类|----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
|----linkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储,线程不安全的
|----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储
2.3.2.2 源码分析-
ArrayList
-
jdk 7情况下
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData list.add(123);//elementData[0] = new Integer(123); ... list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
扩容机制:扩容为原来容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)。
-
jdk8情况下
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组 list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]扩容机制:与jdk 7 相同。
-
总结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
-
-
linkedList
linkedList list = new linkedList(); //内部声明了Node类型的first和last属性,默认值为null list.add(123);//将123封装到Node中,创建了Node对象。 //Node:linkedList的双向链表的一个节点 private static class Node
{ E item; Node next; Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } } -
Vector
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
扩容机制:默认扩容为原来的数组长度的2倍。
Set存储无序的、不可重复的元素。存储顺序是根据数据的哈希值确定的。
以HashSet为例,描述元素添加过程:
- 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即索引位置);
- 判断数组此位置上是否已经元素:
- 如果此位置上没其他元素,则元素a添加成功。
- 如果此位置上其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
- 如果hash值不相同,则元素a添加成功。
- 如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。
在此位置已有元素的情况下,元素添加成功:元素a 与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素。
jdk 8 :原来的元素在数组中,指向元素a
总结:七上八下,如下图
在JDK7中,HashSet底层的数据结构是“数组+链表”;
在JDK8中,HashSet底层的数据结构仍然是“数组+链表”,不过,当链表长度大于8时,链表会转换为红黑树,即数据结构变为了“数组+红黑树”。
2.3.3.1 Set接口常用实现类|----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
|----linkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。
对于频繁的遍历操作,linkedHashSet效率高于HashSet.
|----TreeSet:可以照添加对象的指定属性,进行排序。
2.3.3.2 存储对象所在类的要求-
HashSet/linkedHashSet:
要求向Set(主要指:HashSet、linkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()方法。
重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码。重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
-
TreeSet:
- 在自然排序中,比较两个对象是否相同的标准为compareTo()返回0。(不再是equals())
- 在定制排序中,比较两个对象是否相同的标准为compare()返回0。(不再是equals())
//方式一:自然排序
@Test
public void test1(){
TreeSet set = new TreeSet();
//失败:不能添加不同类的对象
// set.add(123);
// set.add(456);
// set.add("AA");
// set.add(new User("Tom",12));
//举例一:
// set.add(34);
// set.add(-34);
// set.add(43);
// set.add(11);
// set.add(8);
//举例二:
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
//方式二:定制排序
@Test
public void test2(){
Comparator com = new Comparator() {
//照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
2.3.4 Map接口
2.3.4.1 常用实现类
|----Map:双列数据,存储key-value对的数据
|----HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
|----linkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历。
原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
|----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序。底层使用红黑树
|----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|----Properties:常用来处理配置文件。key和value都是String类型
2.3.4.2 存储结构的理解Map中的key:无序的、不可重复的,使用Set存储的key —> key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储的value —>value所在的类要重写equals()
一个键值对:key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所的entry
2.3.4.3 HashMap的实现原理-
JDK7中:
HashMap map = new HashMap()://在实例化以后,底层创建了长度是16的一维数组Entry[] table。 //...可能已经执行过多次put... map.put(key1,value1);
-
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置;
-
如果此位置上的数据为空,此时的key1-value1添加成功。
-
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
-
如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。
-
如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
如果equals()返回false:此时key1-value1添加成功。
如果equals()返回true:使用value1替换value2。
-
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。
-
-
JDK8中:
- new HashMap():底层没创建一个长度为16的数组
- jdk 8底层的数组是:Node[],而非Entry[]
- 首次调用put()方法时,底层创建长度为16的数组
- jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
- 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
HashMap底层典型属性的属性的说明:
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
linkedHashMap底层使用的结构与HashMap相同,因为linkedHashMap继承于HashMap.
区别在于:linkedHashMap内部提供了Entry,替换HashMap中的Node.
向TreeMap中添加key-value,要求key必须是由同一个类创建的对象。因为要根据key进行排序:自然排序 、定制排序
2.3.7 Collections工具类-
常用方法
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所旧值ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以使用synchronizedList(List list) 和 synchronizedMap(Map map)将ArrayList、HashMap转换为线程安全的。
1.举例: 【Order.java】 public class Order{ String orderName; int orderId; //类的内部结构就可以使用类的泛型 T orderT; public Order(){ //编译不通过 // T[] arr = new T[10]; //编译通过 T[] arr = (T[]) new Object[10]; } public Order(String orderName,int orderId,T orderT){ this.orderName = orderName; this.orderId = orderId; this.orderT = orderT; } //如下的个方法都不是泛型方法 public T getOrderT(){ return orderT; } public void setOrderT(T orderT){ this.orderT = orderT; } //静态方法中不能使用类的泛型。 // public static void show(T orderT){ // System.out.println(orderT); // } public void show(){ //编译不通过 // try{ // }catch(T t){ // } } //泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没任何关系。 //换句话说,泛型方法所属的类是不是泛型类都没关系。 //泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。 public static List copyFromArrayToList(E[] arr){ ArrayList list = new ArrayList<>(); for(E e : arr){ list.add(e); } return list; } } 【SubOrder.java】 public class SubOrder extends Order {//SubOrder:不是泛型类 public static List copyFromArrayToList(E[] arr){ ArrayList list = new ArrayList<>(); for(E e : arr){ list.add(e); } return list; } } //实例化时,如下的代码是错误的 SubOrder o = new SubOrder<>(); 【SubOrder1.java】 public class SubOrder1 extends Order {//SubOrder1 :仍然是泛型类 }
备注:
- 泛型类的构造方法是public Person() {}而不是public Person
(){} - 异常类不能是泛型的;
- 泛型不能使用new T[],但是可以T[] data = (T[]) Object[length]
类A是类B的父类,则A
类A是类B的父类,G和G是没关系的,二者共同的父类是G>
@Test
public void test3(){
List
2.4.2 有限制条件的通配符的使用
? extends A:
G extends A> 可以作为G和G的父类,其中B是A的子类.(G extends A>是A以及A的子类的父类)
? super A:
G super A> 可以作为G和G的父类,其中B是A的父类.(G super A>是A以及A的父类的父类)
@Test
public void test4(){
List extends Person> list1 = null;
List super Person> list2 = null;
List list3 = new ArrayList();
List list4 = new ArrayList();
List
2.5 IO流
-
操作数据单位:字节流、字符流
-
数据的流向:输入流、输出流
-
流的角色:节点流、处理流
-
常用构造器
File(String filePath) File(String parentPath,String childPath) File(File parentFile,String childPath)
-
常用方法
getPath():返回路径。若为绝对路径,则返回绝对路径;若为相对路径,则返回相对路径。
getAbsolutePath():返回路径。若为绝对路径,则返回绝对路径;若为相对路径,则返回user.dir+相对路径。
IDEA中:
如果使用JUnit中的单元测试方法测试,相对路径即为当前Module下。
如果使用main()测试,相对路径即为当前的Project下。
Eclipse中:
不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。
-
FileReader、FileWriter:字符流、节点流
FileWriter(file,false) / FileWriter(file):对原文件的覆盖
FileWriter(file,true):不会对原文件覆盖,而是在原文件基础上追加内容
public void testFileReaderFileWriter() { FileReader fr = null; FileWriter fw = null; try { //1.创建File类的对象,指明读入和写出的文件 File srcFile = new File("hello.txt"); File destFile = new File("hello2.txt"); //不能使用字符流来处理图片等字节数据 // File srcFile = new File("爱情与友情.jpg"); // File destFile = new File("爱情与友情1.jpg"); //2.创建输入流和输出流的对象 fr = new FileReader(srcFile); fw = new FileWriter(destFile); //3.数据的读入和写出操作 char[] cbuf = new char[5]; int len;//记录每次读入到cbuf数组中的字符的个数 while((len = fr.read(cbuf)) != -1){ //每次写出len个字符 fw.write(cbuf,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.关闭流资源 //方式一: // try { // if(fw != null) // fw.close(); // } catch (IOException e) { // e.printStackTrace(); // }finally{ // try { // if(fr != null) // fr.close(); // } catch (IOException e) { // e.printStackTrace(); // } // } //方式二: try { if(fw != null) fw.close(); } catch (IOException e) { e.printStackTrace(); } try { if(fr != null) fr.close(); } catch (IOException e) { e.printStackTrace(); } } } -
FIleInputStream、FileOutputStream:字节流、节点流
2.FileInputStream / FileOutputStream的使用: * 1. 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理 * 2. 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用字节流处理 @Test public void testFileInputOutputStream() { FileInputStream fis = null; FileOutputStream fos = null; try { //1.造文件 File srcFile = new File("爱情与友情.jpg"); File destFile = new File("爱情与友情2.jpg"); //2.造流 fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); //3.复制的过程 byte[] buffer = new byte[5]; int len; while((len = fis.read(buffer)) != -1){ fos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if(fos != null){ //4.关闭流 try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
BufferedReader、BufferedWriter:字符流、缓冲流
3.2 使用BufferedReader和BufferedWriter:处理文本文件 @Test public void testBufferedReaderBufferedWriter(){ BufferedReader br = null; BufferedWriter bw = null; try { //创建文件和相应的流 br = new BufferedReader(new FileReader(new File("dbcp.txt"))); bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt"))); //读写操作 //方式一:使用char[]数组 // char[] cbuf = new char[1024]; // int len; // while((len = br.read(cbuf)) != -1){ // bw.write(cbuf,0,len); // // bw.flush(); // } //方式二:使用String String data; while((data = br.readLine()) != null){ //方法一: // bw.write(data + "n");//data中不包含换行符 //方法二: bw.write(data);//data中不包含换行符 bw.newline();//提供换行的操作 } } catch (IOException e) { e.printStackTrace(); } finally { //关闭资源 if(bw != null){ try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } if(br != null){ try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } -
BufferedInputStream、BufferedOutputStream:字节流、缓冲流
3.1 使用BufferedInputStream和BufferedOutputStream:处理非文本文件 //实现文件复制的方法 public void copyFileWithBuffered(String srcPath,String destPath){ BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //1.造文件 File srcFile = new File(srcPath); File destFile = new File(destPath); //2.造流 //2.1 造节点流 FileInputStream fis = new FileInputStream((srcFile)); FileOutputStream fos = new FileOutputStream(destFile); //2.2 造缓冲流 bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos); //3.复制的细节:读取、写入 byte[] buffer = new byte[1024]; int len; while((len = bis.read(buffer)) != -1){ bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.资源关闭 //要求:先关闭外层的流,再关闭内层的流 if(bos != null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if(bis != null){ try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } //说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略. // fos.close(); // fis.close(); } } -
缓冲流可以加快流的读取、写入的速度。因为其内部提供了一个缓冲区。默认情况下是8kb。
转换流提供字节流与字符流之间的转换。
-
InputStreamReader:将一个字节的输入流转换为字符的输入流
解码:字节、字节数组 —>字符数组、字符串 -
OutputStreamWriter:将一个字符的输出流转换为字节的输出流
编码:字符数组、字符串 —> 字节、字节数组
- 实例:
@Test
public void test2() throws Exception {
//1.造文件、造流
File file1 = new File("dbcp.txt");
File file2 = new File("dbcp_gbk.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
//2.读写过程
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
osw.write(cbuf,0,len);
}
//3.关闭资源
isr.close();
osw.close();
}
常见的编码表
ASCII:美国标准信息交换码。用一个字节的7位可以表示。
ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。
GB2312:中国的中文编码表。最多两个字节编码所有字符
GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码
Unicode:国际标准码,融合了目前人类使用的所字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
-
标准的输入输出流
System.in:标准的输入流,默认从键盘输入
System.out:标准的输出流,默认从控制台输出修改默认的输入和输出行为:
System类的setIn(InputStream is) / setOut(PrintStream ps)方式重新指定输入和输出的流。 -
打印流
PrintStream 和PrintWriter
提供了一系列重载的print()和println()方法,用于多种数据类型的输出。
System.out返回的是PrintStream的实例
-
数据流
DataInputStream 和 DataOutputStream用于读取或写出基本数据类型的变量或字符串
@Test public void test3() throws IOException { //1. DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt")); //2. dos.writeUTF("刘建辰"); dos.flush();//刷新操作,将内存中的数据写入文件 dos.writeInt(23); dos.flush(); dos.writeBoolean(true); dos.flush(); //3. dos.close(); } @Test public void test4() throws IOException { //1. DataInputStream dis = new DataInputStream(new FileInputStream("data.txt")); //2. String name = dis.readUTF(); int age = dis.readInt(); boolean isMale = dis.readBoolean(); System.out.println("name = " + name); System.out.println("age = " + age); System.out.println("isMale = " + isMale); //3. dis.close(); }
ObjectOutputStream:内存中的对象—>存储中的文件、通过网络传输出去(序列化过程)
ObjectInputStream:存储中的文件、通过网络接收过来 —>内存中的对象(反序列化过程)
-
对象的序列化机制
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
-
实现序列化的对象所属的类需要满足以下条件
- 实现接口:Serializable
- 当前类提供一个全局常量:serialVersionUID
- 除了当前类需要实现Serializable接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)
ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量。
-
序列化代码实现
@Test public void testObjectOutputStream(){ ObjectOutputStream oos = null; try { //1. oos = new ObjectOutputStream(new FileOutputStream("object.dat")); //2. oos.writeObject(new String("我爱北京天安门")); oos.flush();//刷新操作 oos.writeObject(new Person("王铭",23)); oos.flush(); oos.writeObject(new Person("张学良",23,1001,new Account(5000))); oos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if(oos != null){ //3. try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } -
反序列化代码实现
@Test public void testObjectInputStream(){ ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream("object.dat")); Object obj = ois.readObject(); String str = (String) obj; Person p = (Person) ois.readObject(); Person p1 = (Person) ois.readObject(); System.out.println(str); System.out.println(p); System.out.println(p1); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if(ois != null){ try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
RandomAccessFile直接继承于java.lang.Object类,实现了DataInput和DataOutput接口。
RandomAccessFile既可以作为一个输入流,又可以作为一个输出流。
如果RandomAccessFile作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建;如果写出到的文件存在,则会对原文件内容进行覆盖。(默认情况下,从头覆盖)
可以通过seek(int pos)操作,实现RandomAccessFile“插入”数据的效果。
实例1:
@Test
public void test1() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
//1.
raf1 = new RandomAccessFile(new File("爱情与友情.jpg"),"r");
raf2 = new RandomAccessFile(new File("爱情与友情1.jpg"),"rw");
//2.
byte[] buffer = new byte[1024];
int len;
while((len = raf1.read(buffer)) != -1){
raf2.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//3.
if(raf1 != null){
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(raf2 != null){
try {
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
实例2:(使用RandomAccessFile实现数据的插入效果)
@Test
public void test3() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("hello.txt","rw");
raf1.seek(3);//将指针调到角标为3的位置
//保存指针3后面的所数据到StringBuilder中
StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
byte[] buffer = new byte[20];
int len;
while((len = raf1.read(buffer)) != -1){
builder.append(new String(buffer,0,len)) ;
}
//调回指针,写入“xyz”
raf1.seek(3);
raf1.write("xyz".getBytes());
//将StringBuilder中的数据写入到文件中
raf1.write(builder.toString().getBytes());
raf1.close();
}
执行前:
执行后:
2.5.8 Path、Paths、Files-
Path
替换原有的File类。
-
Paths
常用方法:
-
Files
工具类,常用方法:
InetAddress类:此类的一个对象就代表着一个具体的IP地址。
创建InetAddress对象:
getByName(String host) 、 getLocalHost()
常用方法:
getHostName() / getHostAddress()
端口号与IP地址的组合得出一个网络套接字:Socket
端口号为一个 16 位的整数 0~65535。
2.6.2 TCP网络编程//示例1:客户端发送信息给服务端,服务端将数据显示在控制台上
//客户端
@Test
public void client() {
Socket socket = null;
OutputStream os = null;
try {
//1.创建Socket对象,指明服务器端的ip和端口号
InetAddress inet = InetAddress.getByName("192.168.14.100");
socket = new Socket(inet,8899);
//2.获取一个输出流,用于输出数据
os = socket.getOutputStream();
//3.写出数据的操作
os.write("你好,我是客户端mm".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源的关闭
if(os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务端
@Test
public void server() {
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1.创建服务器端的ServerSocket,指明自己的端口号
ss = new ServerSocket(8899);
//2.调用accept()表示接收来自于客户端的socket
socket = ss.accept();
//3.获取输入流
is = socket.getInputStream();
//不建议这样写,可能会乱码
// byte[] buffer = new byte[1024];
// int len;
// while((len = is.read(buffer)) != -1){
// String str = new String(buffer,0,len);
// System.out.print(str);
// }
//4.读取输入流中的数据
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[5];
int len;
while((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(baos != null){
//5.关闭资源
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(ss != null){
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//示例2:客户端发送文件给服务端,服务端将文件保存在本地。
@Test
public void client() throws IOException {
//1.
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
//2.
OutputStream os = socket.getOutputStream();
//3.
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
//4.
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//5.
fis.close();
os.close();
socket.close();
}
@Test
public void server() throws IOException {
//1.
ServerSocket ss = new ServerSocket(9090);
//2.
Socket socket = ss.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream(new File("beauty1.jpg"));
//5.
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
//6.
fos.close();
is.close();
socket.close();
ss.close();
}
代码示例3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。
@Test
public void client() throws IOException {
//1.
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
//2.
OutputStream os = socket.getOutputStream();
//3.
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
//4.
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//关闭数据的输出
socket.shutdownOutput();
//5.接收来自于服务器端的数据,并显示到控制台上
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bufferr = new byte[20];
int len1;
while((len1 = is.read(buffer)) != -1){
baos.write(buffer,0,len1);
}
System.out.println(baos.toString());
//6.
fis.close();
os.close();
socket.close();
baos.close();
}
@Test
public void server() throws IOException {
//1.
ServerSocket ss = new ServerSocket(9090);
//2.
Socket socket = ss.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream(new File("beauty2.jpg"));
//5.
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("图片传输完成");
//6.服务器端给予客户端反馈
OutputStream os = socket.getOutputStream();
os.write("你好,美女,照片我已收到,非常漂亮!".getBytes());
//7.
fos.close();
is.close();
socket.close();
ss.close();
os.close();
}
2.6.3 UDP网络编程
//发送端
@Test
public void sender() throws IOException {
DatagramSocket socket = new DatagramSocket();
String str = "我是UDP方式发送的导弹";
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);
socket.send(packet);
socket.close();
}
//接收端
@Test
public void receiver() throws IOException {
DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
socket.receive(packet);
System.out.println(new String(packet.getData(),0,packet.getLength()));
socket.close();
}
2.6.4 URL编程
-
URL(Uniform Resource Locator)的理解:
统一资源定位符,对应着互联网的某一资源地址 -
URL的五个结构
http://localhost:8080/examples/beauty.jpg?username=Tom
协议 //主机名 : 端口号 / 资源地址? 参数列表
-
实例化
URL url = new URL("http://localhost:8080/examples/beauty.jpg?username=Tom"); -
常用方法
-
读取、下载对应的url资源:
public static void main(String[] args) { HttpURLConnection urlConnection = null; InputStream is = null; FileOutputStream fos = null; try { URL url = new URL("http://localhost:8080/examples/beauty.jpg"); urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.connect(); is = urlConnection.getInputStream(); fos = new FileOutputStream("day10\beauty3.jpg"); byte[] buffer = new byte[1024]; int len; while((len = is.read(buffer)) != -1){ fos.write(buffer,0,len); } System.out.println("下载完成"); } catch (IOException e) { e.printStackTrace(); } finally { //关闭资源 if(is != null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(fos != null){ try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if(urlConnection != null){ urlConnection.disconnect(); } } }
反射被视为动态语言的关键。
-
获取Class实例的几种方式:
//方式一:调用运行时类的属性:.class Class clazz1 = Person.class; System.out.println(clazz1); //方式二:通过运行时类的对象,调用getClass() Person p1 = new Person(); Class clazz2 = p1.getClass(); System.out.println(clazz2); //方式三:调用Class的静态方法:forName(String classPath) Class clazz3 = Class.forName("com.atguigu.java.Person"); // clazz3 = Class.forName("java.lang.String"); System.out.println(clazz3); System.out.println(clazz1 == clazz2); System.out.println(clazz1 == clazz3); //方式四:使用类的加载器:ClassLoader (了解) ClassLoader classLoader = ReflectionTest.class.getClassLoader(); Class clazz4 = classLoader.loadClass("com.atguigu.java.Person"); System.out.println(clazz4); System.out.println(clazz1 == clazz4); -
使用Classloader加载src目录下的配置文件:
@Test public void test2() throws Exception { Properties pros = new Properties(); //此时的文件默认在当前的module下。 //读取配置文件的方式一: // FileInputStream fis = new FileInputStream("jdbc.properties"); // FileInputStream fis = new FileInputStream("src\jdbc1.properties"); // pros.load(fis); //读取配置文件的方式二:使用ClassLoader //配置文件默认识别为:当前module的src下 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); InputStream is = classLoader.getResourceAsStream("jdbc1.properties"); pros.load(is); String user = pros.getProperty("user"); String password = pros.getProperty("password"); System.out.println("user = " + user + ",password = " + password); }
要想newInstance()正常的创建运行时类的对象,要求:
- 运行时类必须提供空参的构造器。
- 具有空参的构造器的访问权限。通常,设置为public。
Class2.7.2 获取运行时类的完整结构clazz = Person.class; Person obj = clazz.newInstance(); System.out.println(obj);
@Test
public void test1(){
Class clazz = Person.class;
//获取属性结构
//getFields():获取当前运行时类及其父类中声明为public访问权限的属性
Field[] fields = clazz.getFields();
for(Field f : fields){
System.out.println(f);
}
System.out.println();
//getDeclaredFields():获取当前运行时类中声明的所属性。(不包含父类中声明的属性
Field[] declaredFields = clazz.getDeclaredFields();
for(Field f : declaredFields){
System.out.println(f);
}
}
@Test
public void test1(){
Class clazz = Person.class;
//getMethods():获取当前运行时类及其所父类中声明为public权限的方法
Method[] methods = clazz.getMethods();
for(Method m : methods){
System.out.println(m);
}
System.out.println();
//getDeclaredMethods():获取当前运行时类中声明的所方法。(不包含父类中声明的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for(Method m : declaredMethods){
System.out.println(m);
}
}
@Test
public void test1(){
Class clazz = Person.class;
//getConstructors():获取当前运行时类中声明为public的构造器
Constructor[] constructors = clazz.getConstructors();
for(Constructor c : constructors){
System.out.println(c);
}
System.out.println();
//getDeclaredConstructors():获取当前运行时类中声明的所的构造器
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
for(Constructor c : declaredConstructors){
System.out.println(c);
}
}
@Test
public void test2(){
Class clazz = Person.class;
Class superclass = clazz.getSuperclass();
System.out.println(superclass);
}
@Test
public void test3(){
Class clazz = Person.class;
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);
}
@Test
public void test4(){
Class clazz = Person.class;
Type genericSuperclass = clazz.getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
//获取泛型类型
Type[] actualTypeArguments = paramType.getActualTypeArguments();
// System.out.println(actualTypeArguments[0].getTypeName());
System.out.println(((Class)actualTypeArguments[0]).getName());
}
@Test
public void test5(){
Class clazz = Person.class;
Class[] interfaces = clazz.getInterfaces();
for(Class c : interfaces){
System.out.println(c);
}
System.out.println();
//获取运行时类的父类实现的接口
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
for(Class c : interfaces1){
System.out.println(c);
}
}
@Test
public void test6(){
Class clazz = Person.class;
Package pack = clazz.getPackage();
System.out.println(pack);
}
@Test
public void test7(){
Class clazz = Person.class;
Annotation[] annotations = clazz.getAnnotations();
for(Annotation annos : annotations){
System.out.println(annos);
}
}
2.7.3 调用运行时类的指定结构
调用指定的属性:
@Test
public void testField1() throws Exception {
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");
//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(p,"Tom");
System.out.println(name.get(p));
}
调用指定的方法:
@Test
public void testMethod() throws Exception {
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
Method show = clazz.getDeclaredMethod("show", String.class);
//2.保证当前方法是可访问的
show.setAccessible(true);
Object returnValue = show.invoke(p,"CHN"); //String nation = p.show("CHN");
System.out.println(returnValue);
System.out.println("*************如何调用静态方法*****************");
// private static void showDesc()
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//如果调用的运行时类中的方法没返回值,则此invoke()返回null
// Object returnVal = showDesc.invoke(null);
Object returnVal = showDesc.invoke(Person.class);
System.out.println(returnVal);//null
}
调用指定的构造器:
@Test
public void testConstructor() throws Exception {
Class clazz = Person.class;
//private Person(String name)
Constructor constructor = clazz.getDeclaredConstructor(String.class);
//2.保证此构造器是可访问的
constructor.setAccessible(true);
//3.调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");
System.out.println(per);
}
2.7.4 动态代理
- 如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象?
通过Proxy.newProxyInstance()实现 - 当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a?
通过InvocationHandler接口的实现类及其方法invoke() - 实例:
-
MyActiveProxyFactory.java
package cn.qujialin.activeproxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //代理工厂 public class MyActiveProxyFactory { public static Object getProxyInstance(Operator op){ Object proxy = null; ClassLoader cl = op.getClass().getClassLoader(); Class[] interfaces = op.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); System.out.println("methodName:::" + methodName); Object result = method.invoke(op, args); System.out.println("methodName:::" + methodName); return result; } }; return Proxy.newProxyInstance(cl,interfaces,invocationHandler); } } -
Operator.java
package cn.qujialin.activeproxy; //接口 public interface Operator { public int add(int i, int j); public int sub(int i, int j); public int mul(int i, int j); public int div(int i, int j); } -
MyOperator.java
package cn.qujialin.activeproxy; import cn.qujialin.activeproxy.Operator; //被代理类 public class MyOperator implements Operator{ @Override public int add(int i, int j){ System.out.println("add......"); return i + j; } @Override public int sub(int i, int j){ System.out.println("sub......"); return i - j; } @Override public int mul(int i, int j){ System.out.println("mul......"); return i * j; } @Override public int div(int i, int j){ System.out.println("div......"); return i / j; } } -
TestMain.java
package cn.qujialin.activeproxy; //测试 public class TestMain { public static void main(String[] args) { Operator p = (Operator)MyActiveProxyFactory.getProxyInstance(new MyOperator()); p.add(3,5); } }
你学会了吗?收藏点赞加关注,技术学习不迷路~
微信搜索“五维星空”关注我吧~
by 五维星空-分享前后端技术



