/**
- @创建人 zyh
- @创建时间 ${DATE}
- @描述
*/
类头注释:打开file->setting->Editor->Filr and Code Templates->Includes->File Header
快速创建方法file->setting->Editor->LiveTemplates
快速创建Test
匿名对象:只创建对象,但是不用变量接收
正常: Dog d = new Dog(); 匿名: new Dog();
匿名的特点:只能使用一次,每次使用匿名对象时,实际都是创建了一个新的对象
匿名对象也可以作为方法的参数和返回值
继承:在Java中,类的继承是指在一个现有类的基础上去构建一个新的类,构建出来的新类被称作子类,现有类被称作父 类,子类会自动拥有父类所有可继承的属性和方法。
2.1 格式
public class 父类{
....
}
public class 子类 extends 父类{
只要继承父类,那么子类就可以拥有并直接使用,父类中所有非私有的成员私有也可以使用,只是不能直接调用,可以间接使用
}
2.2 继承的好处
a.提高代码的复用性
b.为多态提供了前提
2.3 继承后的特点——成员变量**********
a.如果子父类的成员变量名不同,那么写哪个成员变量就是访问哪个成员变量
b.如果子父类的成员变量名相同,那么根据就近原则,优先访问子类的成员变量
2.4 继承后的特点——成员方法**********
a.如果子父类的成员方法名不同,那么写哪个成员方法,就调用哪个成员方法
b.如果子父类的成员方法名相同,那么根据就近原则,优先调用子类的成员方法
2.5 重写的应用************************
在实际开发中,我们不会定义子父类重名的成员变量的
但是在实际开发中,我们肯定会定义和父类重名的成员方法
2.51方法的重载与重写
方法的重载:在同一个类中,方法名一样,参数列表不一样
方法的重写:在子父类中,子类中的方法和父类中的方法,除了方法体其他一模一样,子类中的该方法我们称为重写的方法
2.6 注意事项
a.子类方法的权限 >= 父类方法的权限
public protected default private
b.重写时,除了权限和方法体,其他必须一模一样
c.私有方法不能被重写
构造方法是不能被子类继承的!!!
在子类的任意构造方法的第一行,有这么一句代码,super(),代表调用父类的无参构造
2.8 super和this
父类空间优先于子类对象产生(内存图)
this 表示整个子类对象
super 表示子类对象中父类的空间
this(参数):调用本类的构造方法
super(参数);调用父类的构造方法
a.Java只支持单继承,不支持多继承(一个子类只能有一个父类) b.Java支持多层继承(子类只能有一个父类,父类也可以有自己的父类) c.Java中有一个类,它是所有类的直接或者间接的父类,称为根类(Object类)
3.1 抽象类的概述
抽象方法: 只有方法的声明(头部),没有方法实现(方法体)
抽象类: 只要该类含有抽象方法,那么该类一定是一个抽象类
3.2 abstract使用格式
抽象方法 public abstract 返回值类型 方法名(参数列表);
抽象类
public abstract class 类名{
}
抽象类的使用
a.抽象类不能创建对象!!!!!
b.抽象类天生就是做父类用的
c.子类继承抽象类后,需要重写所有的抽象方法后,子类才能创建对象,否 则子类还是一个抽象类
抽象类不能创建对象
抽象是有构造的,而是给子类调用的
抽象类可以有抽象方法,也可以没有抽象方法
抽象类的子类,必须重写抽象父类中所有的抽象方法
接口是方法的集合
a.抽象方法(jdk1.7)
b.默认方法和静态方法(jdk1.8)
c.私有方法(jdk1.9)
public interface 接口名{
//接口中只能定义方法
//1.抽象方法
public abstract void abs1();
2.默认方法(default)
public default void show(){
....
}
3.静态方法(static)
public static void show2{
}
}
a.接口不能创建对象(接口中没有构造方法)
b.接口也是天生做父类的,让实现类实现它
a.接口中的所有抽象方法,实现类必须重写
b.接口中的默认方法 实现类可以选择性重写
c.静态方法没有继承的概念
一个类可以实现多个接口
a.实现类必须重写所有抽象方法(如果接口中方法重名,只需重写一次)
b.实现类可以选择重写接口中所有默认方法
如果某个接口中默认方法相同,那么实现类中必须重写一次
c.静态方法没有重写,也不需要重写,因为静态方法通过接口名去直接调用
public class 类名 implements 接口1,接口2{}
一个类可以继承一个父类同事实现多个接口(先写继承再写实现)
如果父类的正常方法和接口默认方法中方法重名时,子类可以不重写,默认使用父类的方法
类和类之间,称为继承,单继承
类和接口之间称为实现,多实现
接口和接口之间称为继承,多继承
接口中不能有普通的成员变量,可以有特殊的“成员变量”,我们称为常量
常量必须有三个修饰符修饰:public static final 数据类型 常量名= 值;
接口中没有构造方法
接口中没有静态代码块
1.9 instanceof 关键字
多态有两个前提
a.必须有继承或者实现
b.必须有方法的重写
多态的体现:
父类类型的变量 指向了 子类类型的对象
直接多态
Animal a = new Dog();
间接多态
Dog d = new Dog();
Aaimal a = d;
方法调用时的多态
method(new Dog());
public static void method(Animal a){}
编译时看父类,运行时看子类
使用父类类型的变量可以接收任何一个子类对象
提高代码的灵活性(扩展性)
多态只能调用父类共有的方法,不能调用字类特有的方法
向上转型:子类-->父类 Animal an = new Dog();
向下转型: 父类-->子类 Dog d = (Dog)an;
当多态需要调用子父类共有方法是,不需要转型
当多态需要调用子类特有方法是,需要向下转型,转成子类
8.转型是出现的问题
类型转换异常:ClassCastException
如实际上是狗类转换为猫类
9.instanceof 关键字
instanceof :是不是谁的对象
用法: boolean b = 对象名 instanceof 类名
Animal an = new Cat();
if(an instanceof Dog){}
a.final修饰类:'太监类-----被final修饰的类不能被其他类继承'
b.final修饰'方法
被final修饰的方法,子类可以继承但是不能重写
c.final修饰'局部变量(局部变量)''
被final修饰的局部变量只能赋值一次
d.final修饰'引用类型的变量'
被final修饰的局部变量(引用类型)只能赋值一次'地址'
final Dog dd = new Dog();
dd = new Dog();//报错,因为企图修改dd中的地址
Dog ddd = dd;
ddd = new Dog();//不报错,因为ddd没有被final修饰
e.final修饰'成员变量'
被final修饰的成员变量'只能赋值一次'
两种方式:a."定义时赋值 ' b.定义时不赋值,在'构造方法中赋值'
static叫静态关键字
能修饰的成员'成员变量和成员方法'
特点:a.'被static修饰的成员,不属于对象,属于整个类'
b.'被static修饰的成员,通过类名就可以直接调用'
被static修饰的成员变量:'不属于对象,属于整个类'
该成员变量,不建议通过对象访问('也可以,强行写'),"建议直接类名访问"
a.该成员方法也不属于对象,属于类
b.该成员方法可以通过类名直接调用
static可以修饰成员变量:类变量
可以通过 "对象名.静态变量名"
建议通过 "类名.静态成员变量名"
static修饰成员方法:"类方法,静态方法"
可以通过 "对象名.静态成员方法名()"
建议通过 "类名.静态成员方法名()"
总结:"静态成员优先于对象存在,在没有对象的情况下,也可以通过类名调用"
静态和非静态的访问问题
a."静态 可以访问 静态"
b."非静态 可以访问 非静态"
c.静态 '不可以访问 非静态'(非静态在创建对象后才存在,而静态是 属于类的,没有对象也可以通过类访问)
d."非静态 可以访问 静态"
由static修饰的代码块
格式 :
static{
静态代码块...
}
位置:类中方法外
特点:"当第一次使用该类(加载到内存时),该类的静态代码块会自动执行(只执行一次),而且静态代码块优 先于构造方法执行的"
作用:用给类初始化的,比如,加载驱动,读取配置文件等...
权限从大到小依次为:
public(公共的) protected(受保护的)default(默认的,空的,不写) private(私有的)
i.成员变量,使用 private
ii.成员方法,使用 public
iii.构造方法,使用 public
内部类中访问外部变量相当于在变量前加finally,是不能改变变量的值的,若是引用变量则地址值不能改变,成员值可改变
定义在外部类的类中方法外
特点:a."内部类中可以无条件的访问外部类中的任何成员"
b.如何创建内部类对象
外部类名.内部类名 对象名 = new 外部类().new 内部类();
定义在方法中,只能在局部使用
是一种快速创建对象的语法
匿名内部类,可以帮助我们"快速创建一个类的子类对象或者一个接口的实现对象"
"快速创建抽象类的子类对象"
抽象类名 对象名 = new 抽象类名(){
重写抽象类中个的抽象方法
};
"快速创建接口的实现类对象"
接口名 对象名 = new 借口名(){
重写接口中的抽象方法
};
基本类型可以做的引用类型也可以做
引用类型作为方法的参数和返回值
包:java.lang
作用:所有类的根类(包括数组)它一共11个方法
public String toString();
作用:返回对象的字符串表示
默认表示格式:"包名.类名@地址值"
开发中会重写toString,返回的不是地址值,而是返回"对象的所有属性值"
自动生成toString的方法:alt+INS,选择需要的成员属性即可
"注意:我们直接使用输出语句输出对象名的时候,其实通过该对象调用了 其toString()方法"
public boolean equals(Object obj);
作用:判断其他某个对象是否与此对象"相等"。
Object 中 equals 方法默认比较的的是"地址值!
开发中:我们通常会重写equals,比较的不是地址值,而是两个对象的所有属性,只有所有属性都相等,两个对象才相等,才会返回true
自动生成equals的方法:alt+INS,选择需要的成员属性即可
==与equals的区别
a.基本类型 只有==,比较的是数值
b.引用类型
对象1 == 对象2 比较就是地址
对象.equals(对象2) "如果没有重写,比较的是地址"
"如果重写,比较的是属性值"
包:"java.util"
作用:类Date表示特定的瞬间,精确到毫秒
构造:
public Date();"当前的系统时间"
public Date(long time);创建一个距离标准时间time毫秒后的时 间 "标准时间:1970-01-01 00:00:00"英国
"标准时间:1970-01-01 08:00:00"(北京时间)
常用方法:
public long getTime();获取Date对象的毫秒值
public void setTime(long time);设置Date对象的毫秒值
包:java.text
作用:格式化Date对象 把Date对象转化成我们喜欢的格式的字符串
构造:DateFormat是一个抽象类 我们使用的是它的子类
"SimpleDateFormat"
包:java.text
构造方法:
public SimpleDateFormat(String pattern);创建一个 日期对象,指定格式化模式
成员方法:
public String format(Date date);将一个Date对象 转化成字符串
publicDate parse(String time);将字符串解析为Date
y | 年 |
---|---|
M | 月 |
d | 日 |
H | 时 |
m | 分 |
s | 秒 |
包:java.util
作用:代表某个瞬间,精确到毫秒
构造:Calendar是一个抽象类
a.获取方式:
通过一个静态方法获取
public static Calendar getInstance();获取一个 Calendar 的实例对象(默认语言是时区)
b.常用方法:
1. public int get(int field);获取日历对象中某一个字段的值
Calendar c = Calendar.getInstance();
例: int year = c.get(Calendar.YEAR);
2. public void set(field int value);修改某一个字段的值
3. public void add(field int value);增加某一个字段的值;
4. public Date getTime();将日历对象转为日期对象
包:"java.lang"
作用:包含用于执行基本数学运算的方法
构造:Math类的构造私有化,不允许任何人创建对象
成员方法:都是静态方法
8个方法: "Math._"
public static int abs(int a);获取绝对值的方法
int abs = Math.abs(1);
public static int max(int a,int b);获取最大值
public static int min(int a,int b);获取最小值
public static int round(float a);获取四舍五入值
public static double random(float a);获取随机小数
范围[0.0,1.0) '即包含0.0不包含1.0'
public static double ceil(double a);"向上取整"
public static double floor(double a);"向下取整"
public static double pow(double a,double b);"求次幂"
包:java.lang
作用:包含一些有用的方法,而且不能创建对象
构造:System类构造私有化,不允许任何人创建对象
方法:两个静态方法
1 arraycopy(源数组,开始下标,目标数组,开始下标,长度);复制数组
2 exit(int code);退出JVM
public static long currentTimeMillis();获取当前系统时间的毫秒值
包装类的介绍
基本类型对应引用类型(基本数据类型的包装类)
char ->Character int->Integer
Integerd 构造和静态方法
装箱:把基本类型 转成 对应的包装类型
拆箱:把包装类型 转成 对应的基本类型
JDK1.5之后基本类型和包装类之间的运算,可以当做基本类型计算
基本类型-->String
转换方式
方式一:直接在数字后加一个空字符串
int number = 100;
String s1 = number + "";
方式二:通过String类静态方法valueOf()
String s2 = String.valueOf(number);
String-->基本类型
转换方式
方式一:先将字符串数字转成Integer,再调用valueOf()方法.
String s = "100";
Integer i = Integer.valueOf(s);
int x = i.intValue();
****方式二:"通过Integer静态方法parseInt()进行转换
int y = Integer.parseInt(s);
程序运行过程中出现的问题
Throwable:所有异常的根类
|-Error 错误(硬件问题)
|-Exception 异常(程序员问题)
|-编译时异常(CheckException):Exception及
其子类("RuntimeException除外)
在编译时就提示异常,称为编译时异常
|-运行时异常(RuntimeException):RuntimeException以及子类
在编译时不提示,运行时直接在控制台上打印异常信息
public void printStackTrace();"打印异常的详细信息"
("异常的类型,原因,位置") '常用'
public String getMessage();获取异常的信息(只有异常的原因)
public StringtoString();获取异常的类型和异常描述信息('不用')
i.编译时异常
Exception及其子类("RuntimeException除外)
在编译时就提示异常,称为编译时异常
ii.运行时异常
RuntimeException以及子类
在编译时不提示,运行时直接在控制台上打印异常信息
格式:throw newXxx异常();
Objects 是工具类(所有方法都是静态的)
Objects.requareNonNull(T obj){}
给方法做声明的,声明方法内可能抛出异常,要求该方法的调用者处理
'格式':
public static void 方法名()throws XxxException,YyyException{
方法体;//
}
就是捕获异常对象,异常对象就不会向上抛出了
格式:
try{
可能抛出异常的代码
}catch(XxxException e){
//打印异常信息
e.printStackTrace();
}
其他代码块(不管有无异常都会执行)
1.直接打印异常(开发和测试阶段)
2.写到日志文件(上线阶段)
3.转换成运行时异常(数据库阶段使用)
public static void method01() throws XxException{
}
public static void method02() throws XxException{
}
public static void method03() throws XxException{
}
a.分别出理:一个try一个catch
public static void main(String[] args){
try{
method01();
}catch(XxxException xxx){
//处理
}
...
}
b."一个try多个catch"
try{
method01();
method02();
method03();
}catch(XxxException xxx){
//处理
}catch(YyyException yyy){
//处理
}
...
"假设XxxException extends YyyException"
那么必须先捕获子类异常,再捕获父类异常
c."一次捕获一次处理 "开发中常用"
try{
method01();
method02();
method03();
}catch(Exception xxx){
//此处必须是所有可能异常的共同父类
}
.......
finally 必须要执行的代码
格式:
try{
可能出现异常的代码
}catch(Exception e){
处理异常的代码
}finally{
必须要执行的代码
一般用于释放资源的代码
释放资源:关闭流,关闭数据库连接,关闭网络连接
}
a.运行时异常,编译阶段不需要抛出,也不需要try..catch
b.子类重写后方法的异常,必须是 父类方法异常的子集
c.如果父类的方法没有抛出异常,那么子类重写方法是必须不能抛出,只能捕获
d.多个catch时,前面的异常必须是子类,后面的异常是父类
e.finally可以加,当有资源需要释放是添加finally
1.为什么要自定义异常
因为JDK不能考虑到所有可能的问题
2.自定义异常的步骤
a.自定义异常必须是XXXException(命名)
b.自定义异常必须继承Exception或者RuntimeException
c.自定义异常必须提供至少两个构造(无参,带有异常信息的)
d.在适当的时候抛出自定义异常
并行:多个事件,在同一时刻同事发生
并发:多个事件,在同一时间段内同时发生(交替执行)
'进程:正在运行的程序
'线程:进程中的一个执行单元,完成某个功能的小模块
进程和线程的一些'区别'
进程拥有独立的内存(独立的栈空间和独立的堆空间)
线程拥有独立的栈和共享的堆
'线程调度'
一个CPU(单核),同一个时刻只能执行一句代码
什么是是线程调度:
CPU在不同的线程之间进行快速的切换
'线程调度分类'
a.分时调度:每个线程轮流执行一定时间
b.抢占式调度:每个线程随机获取CPU的时间
API :一种方法是将类声明为Thread的子类
该类应重写Thread类的run方法,接下来可以分配并启动该子类的实例
步骤:
a.子类 继承Thread
b.子类 重写run方法
c.创建子类对象
d.启动线程 调用对象的start方法
包:java.lang
作用:代表java的线程
构造方法:
public Thread();无惨构造创建线程
public Thread(String name);带线程的名字构造
public Thread(Runnable r);指定任务对象创建线程
public Thread(Runnable r,String name);指定任务对象和线程名字创建线程
成员方法:
public void setName(String name);给线程设置名字
public String getName();获取线程名字
"一般用Thread.currentThread().getNmae()
public void start();开启线程
public void run();保存线程任务代码的方法
public static void sleep(long time);使当前正在执行的线程以指定毫秒数暂停
public static Thread currentThread();获取当前线程对象
Thread t = Thread.currentThread();
API:
声明实现Runnable 接口的类
该类实现run方法
然后分配该类的实例
在创建Thread 时作为一个参数传递并启动
步骤:
a.定义实现类 实现Runnable接口
b.实现类重写run方法(写任务代码)
c.创建实现类对象(不是线程对象)
d.创建Thread对象 同时传入实现类对象
e.启动线程对象 t.start();
一种叫继承方法
一种叫实现方式
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1. 适合多个相同的程序代码的线程去共享同一个资源。("可以让多个线程共享一个任务")
2. 可以避免java中的单继承的局限性。
3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
"线程和任务对象可以由程序员自由组合"
4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
总结:在实际开发中首选实现方式
1.继承方式
n ew Thread(){
public void run(){
...//任务代码块
}
}.start();
2.实现方式
new Thread(new Runnable(){
public void run(){
... // 代码块
}
}).start();
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操 作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则 的话就可能影响线程安全。
a.单线程永远不会有安全问题
b.多线程执行相同任务,操作同一个"共享数据",才可能有安全问题
当多个线程访问同一个共享资源时,只能让一个线程操作
a."同步代码块"
格式:
synchronized(锁对象){
需要同步的代码块
}
b."同步方法"
格式:
public synchronized void method(){
需要同步的代码块
}
"相当于使用当前任务对象this作为锁对象"
"补充:如果同步方法是静态的,那么使用当前类.class文件作为锁对象"
c.Lock锁方式("最好,更能体现面向对象")
格式:
Lock lock = new ReentrantLock();
使用:
lock.lock();//加同步锁。
同步代码块
lock.unlock();//释放同步锁
a.新建状态(new) //线程刚被创建,但是并未启动。还没调用start方法。
b.可运行状态(Runnable) //
c.锁阻塞locked) //
d."无限等待(Waiting)""********
i.线程如何进入Waiting(无线等待状态)
1.必须先持有锁对象
2.调用锁对象的wait()方法
3.同时自动释放锁对象
ii.其他线程如何唤醒Waiting状态的线程
1.其他线程也要持有相同的锁对象
2.调用锁对象的notify()方法
3.被唤醒的线程处于锁阻塞状态,只有再次持有锁对象才能进入可运行状态
e.Timed Waiting(计时 等待) //在线程中调用Thread.sleep(毫米值)方法
f.Teminated(被终止) 线程的任务执行完毕或者调用了线程对象stop方法//因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
集合:就是java中可以保存多个数据的容器
集合和数组的区别:
a.数组的长度是固定的,集合的长度可变;
b.数组的元素可以是"基本类型也可以是应用类型",集合必须是"引用类型"(包装类)
2.集合的框架的介绍
List 的特点是"有索引,元素有序、元素可重复;
Set 的特点是"无索引,元素,不可重复"。
增:add(E e);
删:remove(Object obj)
改:无
查:无
其他(常用的7个:下面5个加上面的增删两个):
int size();//长度
void clear();//清空
boolean contains(Object obj);//是否包含某个元素;
isEmpty();//是否为空(size==0?)
Object[] toArray();//转成数组
a.不能再hasNext方法返回false后,继续使用next方法,否则抛出NoSunchElementException
b.不能再迭代器期间,对集合进行增删操作,否则抛出异 ConcurrentModificationException
总结:迭代器就是为了单纯遍历而生的!!
底层采用指针原理;
hasNext只是判断指针后有无元素,指针并没有移动
next会使指针向后移动一个元素,并取出所经过的元素
这是jdk1.5之后特性,它实际上是一种语法糖
格式:
for(数据类型 变量名:Collection集合/数组){
System.out.prinntln(变量名);
}
数组的增强for循环底层不是迭代器
泛型是一种不确定的类型,使用时才把它确定下来
格式: <E>,<Q>,<MVP>,<可以是一个字母,也可以是一个单词>
比如:ArrayList<E>
HashMap<K,V>
将运行时期的异常ClassCastException转移到编译时期变成了编译失败
避免了强制类型转换的麻烦
总结:JDK之后出现了泛型,java强烈建议使用泛型
1.泛型类:类上含有泛型,比如ArrayList
格式: public class 类名<E>{
//.....
}
使用:创建对象时,确定泛型的具体类型即可
类名<String>对象名 = new 类名<String>();
类名<Integer>对象名 = new 类名<Integer>();
2.泛型方法:方法上含有泛型
格式: public <E> void 方法名(E 变量名){
//...
}
使用: 类名 对象名 = new 类名();
对象名.方法名(10);//此时方法的泛型就是
3.泛型接口:接口上含有泛型
格式:public interface<E>{
//...
}
使用:
1.实现接口时,直接指定接口的泛型
public class 实现类 implements 接口名<String>{
//这时实现类不含有泛型,因为已确定为String
}
2.实现接口时不指定接口的泛型,实现类继续含有泛型
public class 实现类<E> implements 接口名<E>{
//这时实现类含有泛型,实际上就是一个泛型类
}
创建泛型类对象时把泛型确定下来即可
通配符:用来通用匹配任何类型的一种符号
格式:<?>
public static void method(ArrayList<?> list){}
也可以声明 public static <T> void method(ArrayList<T> list){}
上限:<? extends Animal>:代表泛型必须是Animal本类或其子类
下线:<? super Dog>:代表泛型必须是Dog本类或其父类
泛型擦除(伪泛型)
就是java容器中存取数据结构的方式。数据用什么样的方式组合在一起。
1.堆栈结构:先进后出
2.队列结构:先进先出
3.数组结构:查询块,增删慢
4.链表结构:元素一般叫节点 特点"查询慢增删块
5.红黑树结构:查询速度非常恐怖
a.有索引
b.有序(指定是存取顺序)
c.元素可重复
Collection接口有常用的7个方法(均和下标无关)
List接口中除了Collection继承的7个方法外,还多了4个和下标有关的增删改查方法
增:add(index,E e);
删:remove(int index);
改:set(index,E e);
查:get(int index);
List接口的常用实现类
a.ArrayList 底层采用数组结构:查询块,增删慢"没有特有方法,完全实现List"一共7+4个常用方法
b."LinkedList" 底层采用链表结构:查询慢,增删块 实现了List接口,一共7+4个常用方法
"LinkedList"还提供了大量和首尾操作相关的"特有方法"
public void addFirst(E e);添加到开头
public void addLast(E e);添加到末尾
public void removeFirst();删除开头
public void removeLast();删除末尾
public void getFirst();获取第一个元素
public void getLast();获取最后一个元素
public E pop();删除某个元素,和removeFirst一样
public void push(E e);添加元素到开头,和addFirst一样
a.无索引
b.无序(LinkedHashSet除外)
c.元素唯一
Set接口继承Collection接口,所以肯定已经有常有的7个方法"没有特有方法"
Set接口的常用实现类
a.HashSet(无索引,"无序",元素唯一)
底层采用哈希表结构:查询块,增删块 无特有方法
b.LinkedHashSet (无索引,"有序",元素唯一)
底层采用链式哈希表,查询块,增删快 无特有方法
底层采用哈希表结构
"如何判断元素是否重复"
比较两个对象的哈希值,再比较equals 只有哈希值一样,并且equals方法返回true,才判断元素重复
i.对象的哈希值(对象的数字指纹)
调用对象的一个hashCode方法返回对象的哈希值
"我们平常打印的地址值其实是假的,其实是哈希值的16进制表示"
我们java中有地址值吗?
"有,对象名中保存的就是地址值,但是打印不出来"
结论:哈希表结构如何保证元素的唯一性?
"比较两个元素 的hash值和equals,只有哈希值一样,并且equals方法返回true"从而判断两个元 素的唯一性
"总结":如果使用哈希结构保存自定义类型,那么为了保存元素的唯一性,
我们必须重写自定义类型中的"hashCode"方法和"equals"方法
LinkedHashSet:底层采用链式哈希表
特点:
格式:
public static void 方法名(数据类型...a){
//...
}
本质:可变参数其实就是"数组"
"注意事项":a.一个方法最多只能有一个可变参数
b.如果一个方法既有可变参数也有正常参数,那么必须把可变参数放后面
可变参数在实际中的应用
AllayList<String> names = new ArrayList<>();
Collections.addAll(names,"jack","rose","...");
包:java.util
作用:工具类,专门操作集合的,其中的方法都是静态的
public static void shuffle(List<?> list);随机打乱集合的顺序
public static <T> void sort(List<T> list);将集合中元素按照默认规则排序。"默认升序"
compare方法
升序:"前减后"
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
二 请简述HashSet去除重复元素的原理。
调用被添加元素的hashCode(),和HashSet中已有元素的hashCode比较是否相同
如果不相同,直接存储
如果相同,调用equals方法比较是否相同
不相同,直接存储元素
相同,认为是同一元素.不存储
File f = new File("d:/aaa/bbb.java");//"d:"+file.separator+"aaa"+file.separator+"bbb.java"
操作路径不能写死,Windows和Linux系统中分隔符不一样
路径分隔符 Windows :分号; Linux:冒号:
文件名称分隔符 Windows :反斜杠:\ Linux:正斜杠:/
File f1 = new File("d:\\aaa\\bbb.java"); '\'是转义字符
键值对的集合,也叫映射的集合
Collection集合中每个元素是单独存在的,Map集合中每个元素是成对出现的
a.Collection集合中每个元素是单独存在的,Map集合中每个元素是成对出现的
b.Map的键要求是唯一的,值是可以重复的,所以我们通过键可以找到唯一确定的值
c.Collection 中的集合称为"单列集合", Map 中的集合称为"双列集合"
底层采用哈希表结构,存取顺序"不一致",键是唯一的
(如果是自定义的类型,为了保证键的唯一性,必须重写hashCode和equals方法)
底层采用链式哈希表结构,存取顺序"一致(有序)",键是惟一的
(如果是自定义的类型,为了保证键的唯一性,必须重写hashCode和equals方法)
增:V put(K key,V value);向集合中添加键值对
删:V remove(K key);删除集合中的键值对,返回被删除的键值对中的值
改:无 "实际上put方法,添加相同的键值是,就相当于修改的方法,同时返回被修改前的value值"
查:V get(K key);根据键从集合中获取集合中对应的值
其他:public boolean containskey(K key);判断集合中是否包含该键
快捷键*("m.keySet().for")
1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。
方法提示: keyset()
2. 遍历键的Set集合,得到每一个键。
3. 根据键,获取键所对应的值。方法提示: get(K key)
快捷键("m.entrySet().for")
public Set<Map.Entry<K,V>> entrySet() : 获取到Map集合中所有的键值对对象的集合(Set集合)。
public K getKey() :获取Entry对象中的键。
public V getValue() :获取Entry对象中的值。
遍历
for(Map.Entry<K,V> entry:entries){
K key = entry.getKey();
V value = entry.getValue();
System.out.println(key+"="+value);
}
三、
java.util.Arrays 此类包含用来操作数组的各种方法,比如排序和搜索等。其所有方法均为静态方法,调用起来 非常简单。
1.操作数组的方法
public static String toString(int[] a) :返回指定数组内容的字符串表示形式。
public static void sort(int[] a) :对指定的 int 型数组按数字升序进行排序
Collection<V> values();
例:Collection<Integer> values = map.values();
containsKey(Object key)方法
如果此映射包含指定键的映射关系,则返回 true
boolean containsKey(Object key)
containsKey(Object key)方法
如果此映射将一个或多个键映射到指定值,则返回 true
boolean containsValue(Object value)
包:java.io
作用:代表文件或文件夹
public File(String path);创建一个File对象**********
public File(String parent,String child);
public File(File parent,String child);
"注意:File对象不会去检测文件是否真的存在"
File f = new File("d:/aaa/bbb.java");
//"d:"+file.separator+"aaa"+file.separator+"bbb.java"
操作路径不能写死,Windows和Linux系统中分隔符不一样
路径分隔符 Windows :分号; Linux:冒号:
文件名称分隔符 Windows :反斜杠:\ Linux:正斜杠:/
File f1 = new File("d:\\aaa\\bbb.java"); '\'是转义字符
绝对路径:以盘符开头的路径 例:"d:\\aaa\\bbb.java"
相对路径:相对于当前项目的根目录 例:1.txt 指定是项目根目录下的1.txt
public String getAbsolutePath();返回此File的绝对路径名字符串。
public String getPath() :将此File转换为路径名字符串。
public long length();只能获取"文件"的大小(字节)**************************
public String getName();获取文件或者文件夹的名字(不带路径)
public boolean exists();判断该File表示的文件或文件夹是否真的存在
public boolean isDirectory() :此File表示的是否为文件夹
public boolean isFile() :此File表示的是否为文件。
public boolean delete();删除由此File表示的文件或"空文件夹"
public boolean createNewFile();当且仅当具有该名称的文件尚不存在时,创建一个新的"空文件
public boolean mkdir() :创建由此File表示的文件夹
public boolean mkdirs() :创建由此File表示的目录,包括任何必需但不存在的父目录。多及目录
public list();列出目录下所有文件和文件夹的名字//基本不用
public listFiles();返回一个File数组,表示该File目录中的所有的子文件或目录
列出目录下所有文件和文件夹的File对象
需求:在目录C:\Users\yingpeng\Desktop\temp\aaa下 搜索所有的.txt文件
public static void main(String[] args) {
// 需求:在目录C:\Users\yingpeng\Desktop\temp\aaa下 搜索所有的.txt文件
File f = new File("C:\\Users\\yingpeng\\Desktop\\temp\\aaa");
//调用方法
printTxtFile(f);
}
//定义方法:搜索指定文件夹下的.txt文件
public static void printTxtFile(File fileDir) {
//1.遍历这个文件夹
File[] files = fileDir.listFiles();
//2.遍历files即可
for (File file : files) {
//3.判断 必须是文件 且 后缀必须是.txt
if (file.isFile() && file.getName().endsWith(".txt")) {
System.out.println("找到了一个.txt文件:"+file.getAbsolutePath());
} else if (file.isDirectory()) {
//4.判断,如果文件夹
printTxtFile(file);
}
}
}
在方法内部调用方法本身
递归很容易引起"StackOverflowError"栈内存溢出错误
正常递归必须有一个"结束条件"!
使用递归的一般步骤
a.定义一个方法
b.在方法中找规律,调用自己
c.给递归一个结束条件
I:Input 输入流
O:Output 输出流 从外界设备到内存,读数据的流
流:把数据比喻成流 从内存到外界设备,写数据的流
a.根据流向分类
输出流
输入流
b.根据流中的数据分类
字节流
字符流
'字节输出流':顶层父类 OutputStream(抽象类)
作用:写出字节数据的
'字节输入流':顶层父类 InputStream(抽象类)
作用:读取字节数据的
'字符输出流':顶层父类 Writer(抽象类)
作用:写出字符数据的
'字节输入流' 顶层父类 Reader(抽象类)
作用:读取字符数据的
******"java中所有流的命名是非常规范的":功能+父类名************
比如:FileOutputStream 以字节为单位写数据
在java开发中有一个思想:万物皆对象
在IO中有一个思想:一切皆字节
在电脑中所有东西最终底层都是以二进制数据保存的
各种类型文件打开是需要专门的软件将二进制数据解析为对应的文件类型
OutputStream 这是所有字节输出的父类(抽象类)
public static void main(String[] args) throws IOException {
//1.创建一个文件的直接输出流对象
FileOutputStream fos = new FileOutputStream("1.txt");
// FileOutputStream fos = new FileOutputStream(new File("1.txt"));
/**
* 以上构造会干三件事!!!!
* a.创建了一个对象fos
* b.如果文件不存在,会自动创建该文件
* 如果文件存在,则会自动清空文件的内容
* c.让fos对象指向该文件
*/
//2.调用fos的写数据方法
//a.写单个字节
// fos.write(97);
//现在需要打开文本时看到97
// fos.write(57);
// fos.write(55);
//现在需要打开文本时看到java
// fos.write(106);
// fos.write(97);
// fos.write(118);
// fos.write(97);
//b.写一个字节数组
byte[] bs = "java".getBytes();
// fos.write(bs);
//c.写字节数组的一部分
fos.write(bs,0,3);
//3.释放资源
fos.close();
}
三个写字节数据的方法
public void write(int b);写出一个字节
public void write(byte[] bs);写出一个字节数组
public void write(byte[] bs,int startIndex,int len);写出字节数组的一部分
包:java.io
作用:操作文件,向文件中写字节数据的
a.构造方法
public FileOutputStream(String filePath);
public FileOutputStream(File flie);
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。
如果有这个文件, 会清空这个文件的数据。
" 以上构造会干三件事!!!!"
* a.创建了一个对象fos
* b.如果文件不存在,会自动创建该文件
* 如果文件存在,则会自动清空文件的内容
* c.让fos对象指向该文件
public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b) :将 b.length字节从指定的字节数组写入此输出流。
b.写数据的三个方法*********
c.如何追加续写
只要使用另外两个构造即可
public FileOutputStream(File file, boolean append) : 创建文件输出流以写入由指定 的File对象表示 的文件。
public FileOutputStream(String name, boolean append) : 创建文件输出流以指定的名 称写入文件。
d.如何换行
只要想文件中写一个换行符即可
Windows:\r\n
linux:\n
MacOs:\r
扩展—FileWriter"换行用”
FileWriter fw = new FileWriter("D:\\Student.txt");
fis.write(System.lineSeparator());
读数据相关的两个方法
public int read():读取一个字节
public int read(byte[] bs);读取一个字节数组,返回实际读取的个数
public void close();释放资源
包:java.io
作用:读取文件,读取字节数据
a.构造方法
public FileInputStream(String pathname);
public FileInputStream(File file);
以上构造会做三件事:
a.创建对象fis
b.如果文件不存在,直接抛出异常FileNotFoundException
如果存在,则打开文件,不会清空!!
c.让fis对象和文件绑定
b.读取一个字节*********************
//====================一次读取一个字节的标准循环======================
int b = 0;//用来保存读取的字节
/**
* (b = fis.read()) != -1
* 以上代码干了三件事!!!
* a.先读 fis.read()
* b.赋值 b = 读取的数据
* c.判断 b != -1
*/
while ((b = fis.read()) != -1) {
System.out.println((char) b);
}
c.读取一个字节数组*********************
//b.读一个字节数组
//================一次读取字节数组的标准循环=====================
int len = 0;//保存实际读取的长度
byte[] bs = new byte[5];//字节数组
/**
* (len = fis.read(bs)) != -1
* 以上代码干了三件事!!!
* a.先读 fis.read(bs)
* b.赋值 len = 实际读取的个数
* c.判断 len != -1
*/
while ((len = fis.read(bs)) != -1) {
System.out.println(new String(bs,0,len));
字节流是以字节为单位操作文件
字符流是以字符为单位操作文件
当读取含有中文文件时,字节流很可能读取一个中文的一部分,打印出来是乱码
所以不许使用字符流才能正确的读取中文
Reader 字符输入流的顶层父类(抽象类)
和读数据相关的两个方法
public int read():读取一个字符
public int read(char[] chs);读取一个字符数组,返回实际读取的字符长度
包:java.io
作用:读取文件,从文件中读取字符数据
构造方法
public FileReader(String pathname);
public FileReader(File file);
以上构造会做三件事:
a.创建对象fr
b.如果文件不存在,直接抛出异常FileNotFoundException
如果存在,则打开文件,不会清空!!
c.让fr对象和文件绑定
b.读取一个字符****************************
//2.读文件的数据
//a.一次读取单个字符
//=====================一次读单个字符的标准循环====================
int ch = 0;//保存读取的字符
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
c.读取一个字符数组**************************
//2.读文件的数据
//a.一次读取单个字符
//b.一次读取一个字符数组
//=================一次读取字符数组的标准循环=====================
int len = 0;//保存实际读取的字符长度
char[] chs = new char[3];//一次读取3个字符
while ((len = fr.read(chs)) != -1) {
System.out.print(new String(chs,0,len));
}
Write 字符输出流的顶层父类
和写字符数据相关的5个方法
public void write(char ch);写单个字符
public void write(char[] chs);写单个字符数组
public void write(char[] chs,int startIndex,int len);写单个字符数组的一部分
public void write(String str);写字符串
public void write(String str,int startIndex,int len);写字符串的一部分
和写字符串无关的两个方法
public void flush();
public void close();
包:java.io
作用:操作文件,向文件中写字符数据
a.构造方法
public FileWrite(String pathname);
public FileWrite(File file);
b.写字符数据的三组方法(5个)
b.读取一个字符****************************
//2.读文件的数据
//a.一次读取单个字符
//=====================一次读单个字符的标准循环====================
int ch = 0;//保存读取的字符
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
c.读取一个字符数组**************************
//2.读文件的数据
//a.一次读取单个字符
//b.一次读取一个字符数组
//=================一次读取字符数组的标准循环=====================
int len = 0;//保存实际读取的字符长度
char[] chs = new char[3];//一次读取3个字符
while ((len = fr.read(chs)) != -1) {
System.out.print(new String(chs,0,len));
}
c.关闭和刷新的区别
flush();刷新缓冲区,不关闭流(对于字符流来说是有用的!!!)
因为"字节流的缓冲区系统会自动刷新"
close();刷新缓冲区,关闭流
d.续写和换行
public FileWrite(String pathname,boolean append);
public FileWrite(File file,boolean append);
如何换行
Windows "\r\n"
linux:\n
MacOs:\r
//JDK1.7之前的处理方式
public static void method01() {
//FileReader
FileReader fr = null;
try {
fr = new FileReader("1.txt");
//1.创建对象
//2.读取数据
int ch = fr.read();
} catch (IOException e) {
e.printStackTrace();//打印异常信息
} finally {
//3.释放资源
try {
if (fr != null) {
fr.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//JDK1.7以及之后的处理
//try with resource
public static void method02() {
//1.创建对象
try (FileReader fr = new FileReader("1.txt"); FileWriter fw = new FileWriter("2.txt")) {
//2.读取数据
int ch = fr.read();
} catch (IOException e) {
e.printStackTrace();
}
}
被称为持久的属性集
属性集:就是键值对的集合,属性集就是Map,只是Map有两个泛型,Properties不需要泛型,键值对都是String
持久:代表Properties可以直接加载硬盘的数据
2.构造方法
public Properties();//创建了一个空的属性集
3.基本保存数据的方法
public void setProperties(String key,String value);//添加属性,也可以当做修改
public String getProperties(String key);//根据属性名获取属性值
public StringPropertyNames();//获取属性名的集合,相当于map的KeySet
public void load(InputStream inStream/Reader) : //从字节输入流中读取键值对 加载配置文件
public store(new FileWrite("1.properties"),String comments);
缓冲流也叫高效流,读写性能比一般流高的多
缓冲流是对普通流的性能增强,方法上基本没有变化
缓冲流的"基本原理",是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次 数,从而提高读写的效率
字节缓冲输入流:BufferedInputStream ------------>InputStream
字节缓冲输入流:BufferedOutputStream ------------> OutputStream
字符缓冲输入流:BufferedReader ------------> Reader
字符缓冲输出流:BufferedWriter ------------>Writer
a.构造
public BufferedOutputStream(OutputStream out);
public BufferedInputStream(OutputStream out);
a.字符缓冲流的构造
public BufferedWrite(Write w);
public BufferedReader(Reader r);
b.字符缓冲流特有的两个方法
BufferedWritez中特有方法
public void newLine();//向文件中写入一个跨平台的换行符
BufferedReader中特有方法
public String readLine() :// 读一行文字。
字符编码:是一套规则,字符和编码之间的对应规则
编码:把字符按规则变成对应的码值 如a-->ASCII--->97
解码:把码值按规则还原到对应的字符 如97-->ASCII-->a
字符集 Charset :也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、 数字等。
常见的字符集合字符编码表
a.ASCII字符集(英文,阿拉伯数字,符号) 对应的字符编码表就是ASCII码表
b.GBxxx字符集
GB2312字符集中包含常用的中文简体汉子:
GBK字符集,是目前我们最常用的中文字符集,大概包含了2万多个中文字符
GB1380字符集,基本包含全部中文,但它是最新的字符集,还没有正式使用
以上所有的字符集,对应的字符编码表称为GBK码表,在GBK编码表中中文占2个字节
c.Unicode字符集 也叫万国字符集
"对应的字符编码表:UTF-8",编码中中文占"3"个字节
由于编码不同,引出的乱码问题
a.Windows 默认使用GBK编码
b.idea 默认使用UTF-8编码
是Reader的子类,是从字节流到字符流的桥梁。"解码"
a.构造方法
InputStreamReader(InputStream in) : 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName) : 创建一个指定字符集的字符流。
作用:是Writer的子类,是从字符流到字节流的桥梁。"编码"
使用指定的字符集将字符 编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
1.构造方法
public OutputStreamWrite(OutputStream o);默认UTF-8
public OutputStreamWrite(OutputStream o,String charseName);
Java中也有一种机制,把序列化版本号交给程序员自己控制
private static final long serialVersionUID = 值;***
序列化也叫对象流
ObjectOutputStream 对象输出流,称为序列化流
ObjectInputStream 对象输入流,称为反序列化流
包:java.io
作用:操作对象,学对象
a.构造方法
public ObjectOutputStream(OutputStream out);
b.序列化操作的前提
要求被序列化的对象必须实现一个接口,java.io.Serializable 接口
2.写出对象方法 public final void writeObject (Object obj) : 将指定的对象写出。
c.序列化操作(代码演示)
public static void main(String[] args) throws IOException {
//1.创建一个对象输出流,序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.txt"));
//2.写数据方法
//NotSerializableException 不能序列化异常
oos.writeObject(new Dog(2, "旺财"));
//3.释放资源
oos.close();
包:java.io
作用:操作对象,读对象
a.构造方法
public ObjectInputStream(InputStream in);
b.反序列操作(正常演示)
public static void main(String[] args) throws IOException, ClassNotFoundException {
//1.创建一个反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.txt"));
//2.读取数据
Object obj = ois.readObject();
System.out.println(obj);
//3.释放资源
ois.close();
}
c.反序列化操作的两种错误演示
a.ClassNotFoundException 类找不到异常
序列化之后,反序列化之前删除类文件
b.InvalidClassException 无效类异常
序列化之后,反序列化之前修改类的文件
d.JVM如何知道类以及修改过了呢???
所有实现java.io.Serializable接口类,都会有一个序列化版本号
当类的内容改变的,那么此序列化版本号会重新计算,与原来就不同了
我们执行反序列化操作时,对比原来的序列化版本和当前的序列化版本号
如果不一样就会抛出InvalidClassException 异常
e.关键字 transient关键字
transient:瞬态关键字
transient关键字可以用来修饰成员变量,
被transient修饰的成员变量和不被transient修饰的成员变量,完全没有区别
在序列化操作,如果该成员变量被transient修饰,那么会忽略该成员变量
1.打印流PrintStream的介绍
包:java.io
作用:可以让程序员很轻松打印各种数据类型
通俗讲:所见即所得
构造方法:
public PrintStream(String pathanme);
public PrintStream(File file);
public PrintStream(OutputStream out);
常用方法:
public void print(各种数据类型);不带换行的打印数据
public void println(各种数据类型); 带有换行的打印数据
public static void main(String[] args) throws FileNotFoundException {
//1.创建一个打印流对象
PrintStream ps = new PrintStream("1.txt");
// PrintStream ps = new PrintStream(new File("1.txt"));
// PrintStream ps = new PrintStream(new FileOutputStream("1.txt"));
//2.打印各种数据类型
ps.println(97);
ps.println('a');
ps.println(3.14);
ps.println("I love you");
//3.释放资源
ps.close();
}
//修改System.out的方向
// System.out = ps;
System.setOut(ps);
System.out.println("jack");
System.out.println("jack");
System.out.println("jack");
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
由于接口当中抽象方法的public abstract
是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
void myMethod();
}
与@Override
注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface
。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
对于刚刚定义好的MyFunctionalInterface
函数式接口,典型使用场景就是作为方法的参数:
public class Demo09FunctionalInterface {
// 使用自定义的函数式接口作为方法参数
private static void doSomething(MyFunctionalInterface inter) {
inter.myMethod(); // 调用自定义的函数式接口方法
}
public static void main(String[] args) {
// 调用使用函数式接口的方法
doSomething(() -> System.out.println("Lambda执行啦!"));
}
}
在兼顾面向对象特性的基础上,Java语言通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。下面我们做一个初探。
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
注:日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:
public class Demo01Logger {
private static void log(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1, msgA + msgB + msgC);
}
}
这段代码存在问题:无论级别是否满足要求,作为log
方法的第二个参数,三个字符串一定会首先被拼接并传入方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
备注:SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进行字符串拼接。例如:
LOGGER.debug("变量{}的取值为{}。", "os", "macOS")
,其中的大括号{}
为占位符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。
使用Lambda必然需要一个函数式接口:
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
然后对log
方法进行改造:
public class Demo02LoggerLambda {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(1, () -> msgA + msgB + msgC );
}
}
这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接。
下面的代码可以通过结果进行验证:
public class Demo03LoggerDelay {
private static void log(int level, MessageBuilder builder) {
if (level == 1) {
System.out.println(builder.buildMessage());
}
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
String msgC = "Java";
log(2, () -> {
System.out.println("Lambda执行!");
return msgA + msgB + msgC;
});
}
}
从结果中可以看出,在不符合级别要求的情况下,Lambda将不会执行。从而达到节省性能的效果。
扩展:实际上使用内部类也可以达到同样的效果,只是将代码操作延迟到了另外一个对象当中通过调用方法来完成。而是否调用其所在方法是在条件判断之后才执行的。
如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。
例如java.lang.Runnable
接口就是一个函数式接口,假设有一个startThread
方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和Thread
类的构造方法参数为Runnable
没有本质区别。
public class Demo04Runnable {
private static void startThread(Runnable task) {
new Thread(task).start();
}
public static void main(String[] args) {
startThread(() -> System.out.println("线程任务执行!"));
}
}
类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个java.util.Comparator
接口类型的对象作为排序器时,就可以调该方法获取。
import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
private static Comparator<String> newComparator() {
return (a, b) -> b.length() - a.length();
}
public static void main(String[] args) {
String[] array = { "abc", "ab", "abcd" };
System.out.println(Arrays.toString(array));
Arrays.sort(array, newComparator());
System.out.println(Arrays.toString(array));
}
}
其中直接return一个Lambda表达式即可。
JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function
包中被提供。下面是最简单的几个接口及使用示例。
java.util.function.Supplier
接口仅包含一个无参的方法:T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
import java.util.function.Supplier;
public class Demo08Supplier {
private static String getString(Supplier<String> function) {
return function.get();
}
public static void main(String[] args) {
String msgA = "Hello";
String msgB = "World";
System.out.println(getString(() -> msgA + msgB));
}
}
使用Supplier
接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer
类。
public class Demo02Test {
//定一个方法,方法的参数传递Supplier,泛型使用Integer
public static int getMax(Supplier<Integer> sup){
return sup.get();
}
public static void main(String[] args) {
int arr[] = {2,3,4,52,333,23};
//调用getMax方法,参数传递Lambda
int maxNum = getMax(()->{
//计算数组的最大值
int max = arr[0];
for(int i : arr){
if(i>max){
max = i;
}
}
return max;
});
System.out.println(maxNum);
}
}
java.util.function.Consumer
接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
Consumer
接口中包含抽象方法void accept(T t)
,意为消费一个指定泛型的数据。基本使用如:
import java.util.function.Consumer;
public class Demo09Consumer {
private static void consumeString(Consumer<String> function) {
function.accept("Hello");
}
public static void main(String[] args) {
consumeString(s -> System.out.println(s));
}
}
当然,更好的写法是使用方法引用。
如果一个方法的参数和返回值全都是Consumer
类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer
接口中的default方法andThen
。下面是JDK的源代码:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
备注:
java.util.Objects
的requireNonNull
静态方法将会在参数为null时主动抛出NullPointerException
异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个Lambda表达式即可,而andThen
的语义正是“一步接一步”操作。例如两个步骤组合的情况:
import java.util.function.Consumer;
public class Demo10ConsumerAndThen {
private static void consumeString(Consumer<String> one, Consumer<String> two) {
one.andThen(two).accept("Hello");
}
public static void main(String[] args) {
consumeString(
s -> System.out.println(s.toUpperCase()),
s -> System.out.println(s.toLowerCase()));
}
}
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。
下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX。
”的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer
接口的Lambda实例,将打印性别的动作作为第二个Consumer
接口的Lambda实例,将两个Consumer
接口按照顺序“拼接”到一起。
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
}
import java.util.function.Consumer;
public class DemoConsumer {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男" };
printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
s -> System.out.println("。性别:" + s.split(",")[1] + "。"),
array);
}
private static void printInfo(Consumer<String> one, Consumer<String> two, String[] array) {
for (String info : array) {
one.andThen(two).accept(info); // 姓名:迪丽热巴。性别:女。
}
}
}
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate
接口。
Predicate
接口中包含一个抽象方法:boolean test(T t)
。用于条件判断的场景:
import java.util.function.Predicate;
public class Demo15PredicateTest {
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() > 5);
}
}
条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate
条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and
。其JDK源码为:
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
如果要判断一个字符串既要包含大写“H”,又要包含大写“W”,那么:
import java.util.function.Predicate;
public class Demo16PredicateAnd {
private static void method(Predicate<String> one, Predicate<String> two) {
boolean isValid = one.and(two).test("Helloworld");
System.out.println("字符串符合要求吗:" + isValid);
}
public static void main(String[] args) {
method(s -> s.contains("H"), s -> s.contains("W"));
}
}
与and
的“与”类似,默认方法or
实现逻辑关系中的“或”。JDK源码为:
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
如果希望实现逻辑“字符串包含大写H或者包含大写W”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:
import java.util.function.Predicate;
public class Demo16PredicateAnd {
private static void method(Predicate<String> one, Predicate<String> two) {
boolean isValid = one.or(two).test("Helloworld");
System.out.println("字符串符合要求吗:" + isValid);
}
public static void main(String[] args) {
method(s -> s.contains("H"), s -> s.contains("W"));
}
}
“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法negate
的JDK源代码为:
default Predicate<T> negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在test
方法调用之前调用negate
方法,正如and
和or
方法一样:
import java.util.function.Predicate;
public class Demo17PredicateNegate {
private static void method(Predicate<String> predicate) {
boolean veryLong = predicate.negate().test("HelloWorld");
System.out.println("字符串很长吗:" + veryLong);
}
public static void main(String[] args) {
method(s -> s.length() < 5);
}
}
数组当中有多条“姓名+性别”的信息如下,请通过Predicate
接口的拼装将符合要求的字符串筛选到集合ArrayList
中,需要同时满足两个条件:
public class DemoPredicate {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class DemoPredicate {
public static void main(String[] args) {
String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
List<String> list = filter(array,
s -> "女".equals(s.split(",")[1]),
s -> s.split(",")[0].length() == 4);
System.out.println(list);
}
private static List<String> filter(String[] array, Predicate<String> one,
Predicate<String> two) {
List<String> list = new ArrayList<>();
for (String info : array) {
if (one.and(two).test(info)) {
list.add(info);
}
}
return list;
}
}
java.util.function.Function
接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。
Function
接口中最主要的抽象方法为:R apply(T t)
,根据类型T的参数获取类型R的结果。
使用的场景例如:将String
类型转换为Integer
类型。
import java.util.function.Function;
public class Demo11FunctionApply {
private static void method(Function<String, Integer> function) {
int num = function.apply("10");
System.out.println(num + 20);
}
public static void main(String[] args) {
method(s -> Integer.parseInt(s));
}
}
当然,最好是通过方法引用的写法。
Function
接口中有一个默认的andThen
方法,用来进行组合操作。JDK源代码如:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
该方法同样用于“先做什么,再做什么”的场景,和Consumer
中的andThen
差不多:
import java.util.function.Function;
public class Demo12FunctionAndThen {
private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
int num = one.andThen(two).apply("10");
System.out.println(num + 20);
}
public static void main(String[] args) {
method(str->Integer.parseInt(str)+10, i -> i *= 10);
}
}
第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过andThen
按照前后顺序组合到了一起。
请注意,Function的前置条件泛型和后置条件泛型可以相同。
请使用Function
进行函数模型的拼接,按照顺序需要执行的多个函数操作为:
String str = "赵丽颖,20";
import java.util.function.Function;
public class DemoFunction {
public static void main(String[] args) {
String str = "赵丽颖,20";
int age = getAgeNum(str, s -> s.split(",")[1],
s ->Integer.parseInt(s),
n -> n += 100);
System.out.println(age);
}
private static int getAgeNum(String str, Function<String, String> one,
Function<String, Integer> two,
Function<Integer, Integer> three) {
return one.andThen(two).andThen(three).apply(str);
}
}
强调:输入量,计算过程,输出量
函数式编程在java中体现就是Lambda表达式
不得不使用java提供的Runnable接口
为了减少实现类文件,不得不创建匿名内部类对象
实现必须重写接口的抽象方法
不得不重写方法,方法必须交run且必须符合接口中抽象方法的格式
实际上我们视乎真正需要的就是run方法中的内容
new Thread(()->{方法体});
Lambda是函数式编程思想在java中的体现,强调:输入量(参数),计算过程(方法体),输出量(返回值)
Lambda标准格式
(参数列表)->{方法体;return 结果;}
解析格式:
(参数列表):如果没有参数,那么只写(),如果有参数(数据类型 参数名,数据类型 参数名)
-> :固定格式
{方法体:return 返回值;}:如果没有方法体,那么方法体也可以省略,如果没有return语句也可以省略
a.参数中数据类型,可以无条件省略
b.如果参数只有一个,那么小括号可以省略
c. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
a.Lambda只能用于替代"接口"的实现类,并且该接口中只能有一个"抽象方法"
b.省略格式,只有上述提到的三点可省略,其他均不可省略(Lambda推导)
有且只有一个抽象方法的接口,称为函数式接口
比如:Runnable接口,Compartor接口
注解:@FunctionalInterface
b.Consumer<T>函数式接口,消费接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public static void main(String[] args) {
//调用方法
//匿名内部类
method(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("长度:"+s.length());
}
});
//Lambda
method(s ->System.out.println("长度:"+s.length()));
}
//定义一个方法
public static void method(Consumer<String> consumer) {
consumer.accept("java");
}
c.Predicate<T>函数式接口,预测接口,判断接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);//判断
}
public static void main(String[] args) {
//调用方法
//匿名内部类
method(new Predicate<String>() {
@Override
public boolean test(String s) {
//比如,字符串需要长度为5
// return s.length() > 5;
//比如,字符串必须以j开头
return s.startsWith("j");
}
});
//Lambda
method(s -> s.startsWith("j"));
}
//定义方法
public static void method(Predicate<String> p) {
//调用p中判断方法test
boolean b = p.test("Hello");
System.out.println("该字符串符合要求吗?" + b);
}
获取Stream流的方式
1.Collection中 方法 Stream stream()
2.Stream接口 中静态方法 of(T...t) 向Stream中添加多个数据
a.普通集合如何获取流
Stream<E> stream = 集合对象.stream();
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();
b.数组如何获取流
Stream<数组的泛型> s1 = stream.of(数组);
Stream<元素的泛型> s1 = stream.of(元素1,元素2,元素n);
c.根据Map获取流
public class Demo05GetStream {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// ...
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
}
//准备一个流对象
Stream<String> stream = Stream.of("张三丰","张无忌","梅超风","灭绝师太","谢逊","成坤");
a.过滤:"filter"
Stream<String> stream1 = stream.filter(s->s.startsWith("张"));
b.统计个数:"count(代码演示)""
long count = stream1.count();
System.out.println(count);
c.取前几个:"limit(代码演示)""
Stream<String> stream2 = stream.limit(3L);
stream2.forEach(s-> System.out.println(s));
d.跳过前几个:"skip(代码演示)""
Stream<String> stream3 = stream.skip(2);
System.out.println(stream3.count());
d.跳过前几个:"skip(代码演示)"
Stream<String> stream3 = stream.skip(2);
System.out.println(stream3.count
e "concat:组合"
ArrayList<String> arr = new ArrayList<String>();
arr.add("jack");
arr.add("tom");
arr.add("marry");
String[] names = {"李雷","韩梅梅","玛丽"};
//a.获取arr的流
Stream<String> stream4 = arr.stream();
//b.获取数组的流
Stream<String> stream5 = Stream.of(names);
//c.合并
"Stream stream6 = Stream.concat(stream4, stream5);"
//打印
stream6.forEach(s -> System.out.println(s));
9.总结:函数拼接和终结方法
返回值还是Stream对象称为,函数拼接方法,支持链式编程
filter方法
limit方法
skip方法
concat方法
返回值不是Stream对象称为,终结方法
foreach方法
count方法
注意:一个Stream对象,只要调用了终结方法,那么该Stream就不能继续使用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
来看一个简单的函数式接口以应用Lambda表达式:
@FunctionalInterface
public interface Printable {
void print(String str);
}
在Printable
接口当中唯一的抽象方法print
接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:
public class Demo01PrintSimple {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
}
其中printString
方法只管调用Printable
接口的print
方法,而并不管print
方法的具体实现逻辑会将字符串打印到什么地方去。而main
方法通过Lambda表达式指定了函数式接口Printable
的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它。
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是System.out
对象中的println(String)
方法。既然Lambda希望做的事情就是调用println(String)
方法,那何必自己手动调用呢?
能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:
public class Demo02PrintRef {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(System.out::println);
}
}
请注意其中的双冒号::
写法,这被称为“方法引用”,而双冒号是一种新的语法。
双冒号::
为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
例如上例中,System.out
对象中有一个重载的println(String)
方法恰好就是我们所需要的。那么对于printString
方法的函数式接口参数,对比下面两种写法,完全等效:
s -> System.out.println(s);
System.out::println
第一种语义是指:拿到参数之后经Lambda之手,继而传递给System.out.println
方法去处理。
第二种等效写法的语义是指:直接让System.out
中的println
方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。
注:Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。
下面这段代码将会调用println
方法的不同重载形式,将函数式接口改为int类型的参数:
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:
public class Demo03PrintOverload {
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
public static void main(String[] args) {
printInteger(System.out::println);
}
}
这次方法引用将会自动匹配到println(int)
的重载形式。
这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:
public class MethodRefObject {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
函数式接口仍然定义为:
@FunctionalInterface
public interface Printable {
void print(String str);
}
那么当需要使用这个printUpperCase
成员方法来替代Printable
接口的Lambda的时候,已经具有了MethodRefObject
类的对象实例,则可以通过对象名引用成员方法,代码为:
public class Demo04MethodRef {
private static void printString(Printable lambda) {
lambda.print("Hello");
}
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
}
由于在java.lang.Math
类中已经存在了静态方法abs
,所以当我们需要通过Lambda来调用该方法时,有两种写法。首先是函数式接口:
@FunctionalInterface
public interface Calcable {
int calc(int num);
}
第一种写法是使用Lambda表达式:
public class Demo05Lambda {
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(-10, n -> Math.abs(n));
}
}
但是使用方法引用的更好写法是:
public class Demo06MethodRef {
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(-10, Math::abs);
}
}
在这个例子中,下面两种写法是等效的:
n -> Math.abs(n)
Math::abs
如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:
@FunctionalInterface
public interface Greetable {
void greet();
}
然后是父类Human
的内容:
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
最后是子类Man
的内容,其中使用了Lambda的写法:
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
//定义方法method,参数传递Greetable接口
public void method(Greetable g){
g.greet();
}
public void show(){
//调用method方法,使用Lambda表达式
method(()->{
//创建Human对象,调用sayHello方法
new Human().sayHello();
});
//简化Lambda
method(()->new Human().sayHello());
//使用super关键字代替父类对象
method(()->super.sayHello());
}
}
但是如果使用方法引用来调用父类中的sayHello
方法会更好,例如另一个子类Woman
:
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("大家好,我是Man!");
}
//定义方法method,参数传递Greetable接口
public void method(Greetable g){
g.greet();
}
public void show(){
method(super::sayHello);
}
}
在这个例子中,下面两种写法是等效的:
() -> super.sayHello()
super::sayHello
this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。首先是简单的函数式接口:
@FunctionalInterface
public interface Richable {
void buy();
}
下面是一个丈夫Husband
类:
public class Husband {
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(() -> System.out.println("买套房子"));
}
}
开心方法beHappy
调用了结婚方法marry
,后者的参数为函数式接口Richable
,所以需要一个Lambda表达式。但是如果这个Lambda表达式的内容已经在本类当中存在了,则可以对Husband
丈夫类进行修改:
public class Husband {
private void buyHouse() {
System.out.println("买套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(() -> this.buyHouse());
}
}
如果希望取消掉Lambda表达式,用方法引用进行替换,则更好的写法为:
public class Husband {
private void buyHouse() {
System.out.println("买套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
marry(this::buyHouse);
}
}
在这个例子中,下面两种写法是等效的:
() -> this.buyHouse()
this::buyHouse
由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名称::new
的格式表示。首先是一个简单的Person
类:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后是用来创建Person
对象的函数式接口:
public interface PersonBuilder {
Person buildPerson(String name);
}
要使用这个函数式接口,可以通过Lambda表达式:
public class Demo09Lambda {
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
printName("赵丽颖", name -> new Person(name));
}
}
但是通过构造器引用,有更好的写法:
public class Demo10ConstructorRef {
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
printName("赵丽颖", Person::new);
}
}
在这个例子中,下面两种写法是等效的:
name -> new Person(name)
Person::new
数组也是Object
的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
在应用该接口的时候,可以通过Lambda表达式:
public class Demo11ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, length -> new int[length]);
}
}
但是更好的写法是使用数组的构造器引用:
public class Demo12ArrayInitRef {
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
}
在这个例子中,下面两种写法是等效的:
length -> new int[length]
int[]::new
C/S结构:Client(客户端)、Server(服务器) 客户端和服务器结构
B/S结构:Browser(客户端)、Server(服务器) 浏览器和服务器结构
2.网络通信协议介绍
互联网上计算机数据交互需要遵循的规则
就像交通管理条例
3.常用的通信协议
IP协议: 规定每台电脑上网需要IP地址
TCP协议:传输控制协议(Transition Control Protocol)
特点:面向有链接,优点:数据是完整,是安全,缺点:性能较低
比如:迅雷下载
UDP协议:用户数据报协议(User Datagram Protocol)
特点:面向无连接,优点:性能高 缺点: 数据不完整,也不安全
比如:供屏软件
其他协议:FTP,WWW,Http,Https,...
4.网络编程的三要素
网络通信协议: TCP和UDP协议
IP地址:每一台连接到互联网的计算机的唯一标识(相当于人的身份证号)
IP地址可以分为两类:IPv4(面临枯竭) 00010010 00101000 00110000 00000111
192.168.100.101
IPv6(128位二进制): 号称可以为世界上每一粒沙子搞一个IP,而且不重复
端口号:一台计算机中不同的应用程序
常用的应用和其端口号
mysql: 3306
Oracle:1521
tomcat:8080
扩展:
a.IP地址的未来
IPv4 --切换-->IPv6
b.IP地址的获取和测试(Dos命令)
ipconfig 查看IP地址
ping IP地址 测试本机网络是否畅通
c.特殊的IP地址
127.0.0.1 本地回环地址
二.TCP通信
1.TCP通信分为客户端和服务器
客户端:一台电脑(个人,配置较低)
服务器:一台电脑(公司,配置很高)
2.TCP中的两个重要的类
Socket: 代表客户端的类,套接字类
ServerScoket:代表服务器的类,服务器套接字类
3.Socket类的介绍和使用
a.构造方法
public Socket(String ip,int port);
ip: 要连接的服务器的IP
port:服务器的端口号
此构造只要创建成功,那么已经连接上服务器了
此构造只要连接不成功,直接抛出异常...
b.常用方法
public OutputStream getOutputStream();获取连接通道中的输出流
public InpuStream getInputStream();获取连接通道中的输入流
public void shutDownOutput();关闭连接通道中输出流
public void shutDownInput();关闭连接通道中输入流
public void close();关闭客户端,释放资源
4.ServerSocket类的介绍和使用
a.构造方法
public ServerSocket(int port);
port:服务器运行的端口号
b.常用的成员方法
public void close();关闭服务器,释放资源
public Socket accept();接收客户端对象
5.简单的TCP通信实现(单向通信)****************************
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.创建客户端对象
Socket client = new Socket("127.0.0.1",12345);
System.out.println("已经成功连接到服务器...");
//2.获取通道中输出流
OutputStream out = client.getOutputStream();
//3.输出流调用write方法,数据发送到服务器
out.write("你好,我是你大爷....".getBytes());
System.out.println("已经向服务器发送信息.....");
//4.释放资源
out.close();
client.close();
}
}
public static void main(String[] args) throws IOException {
//1.创建服务器对象,指定端口号
ServerSocket server = new ServerSocket(12345);
System.out.println("服务器启动成功....");
//2.获取连接到我的那个客户端对象
Socket client = server.accept();//accept方法具有阻塞功能
System.out.println("有客户端连接了..."+client.getInetAddress().getHostAddress());
//3.获取通道中的输入流
InputStream in = client.getInputStream();
//4.调用用输入流的read方法,读取客户端发送来的数据
byte[] bs = new byte[1024];
int len = in.read(bs);
System.out.println("客户端说:"+new String(bs,0,len));
//5.释放资源
in.close();
client.close();
server.close();
}
6.简单的TCP通信实现(双向通信)****************************
public class TCPClient {
public static void main(String[] args) throws IOException {
//1.创建客户端对象
Socket client = new Socket("127.0.0.1",12345);
System.out.println("已经成功连接到服务器...");
//2.获取通道中输出流
OutputStream out = client.getOutputStream();
//3.输出流调用write方法,数据发送到服务器
out.write("你好,我是你大爷....".getBytes());
System.out.println("已经向服务器发送信息.....");
//4.获取通道中输入流
InputStream in = client.getInputStream();
//5.读取服务器反馈的数据
byte[] bs = new byte[1024];
int len = in.read(bs);
System.out.println("服务器说:"+new String(bs,0,len));
//4.释放资源
in.close();
out.close();
client.close();
}
}
public class TCPServer {
public static void main(String[] args) throws IOException {
//1.创建服务器对象,指定端口号
ServerSocket server = new ServerSocket(12345);
System.out.println("服务器启动成功....");
//2.获取连接到我的那个客户端对象
Socket client = server.accept();//accept方法具有阻塞功能
System.out.println("有客户端连接了..."+client.getInetAddress().getHostAddress());
//3.获取通道中的输入流
InputStream in = client.getInputStream();
//4.调用用输入流的read方法,读取客户端发送来的数据
byte[] bs = new byte[1024];
int len = in.read(bs);
System.out.println("客户端说:"+new String(bs,0,len));
//5.获取通道中的输出流
OutputStream out = client.getOutputStream();
//6.向客户端反馈数据
out.write("您的信息我已经收到了,您可以安息了...".getBytes());
//5.释放资源
out.close();
in.close();
client.close();
server.close();
}
}
三.综合案例:文件上传
1.文件上传案例分析(画图演示)
(略)
2.文件上传案例实现(代码演示)
(略,见代码)
3.文件上传案例实现的不足之处
a.服务器保存图片是名字是定死的
b.每次服务完一个客户端之后,服务器关闭了
c.如果文件过大,那么服务器就不能接收下一个客户端了!!!
4.文件上传案例的优化实现(代码演示)
(略,见代码)
5.模拟BS架构服务器
6.扩展:模拟服务器扩展_图片显示问题
总结:
- [ ] 能够辨别UDP和TCP协议特点
TCP:面向有链接
UDP:面向无连接
- [ ] 能够说出TCP协议下两个常用类名称
Socket 代表客户端
ServerSocket 代表服务器端
- [ ] 能够编写TCP协议下字符串数据传输程序
TCP下单向通信和双向通信
- [ ] 能够理解TCP协议下文件上传案例
- [ ] 能够理解TCP协议下模拟B\S服务器案例
client.getInetAddress().getHostAddress()获取客户端的IP地址
作用:简单的来说就是用来代替main方法的
Junit不是JDK中的,需要下载
一般来说开发工具都带有Junit框架
使用步骤
a.编写一个被测试的类(业务类)
b.编写一个测试类(我们以前使用main方法,现在使用Junit框架来测试)
c.编写一个测试方法
运行方法
a.选中方法右键运行
b.选中测试类右键运行
c.选中模块右键运行
3.单元测试其他注解
@Before 该方法会在每一个测试方法前执行
@After 该方法会在每一个测试方法后执行
@BeforeClass 该方法会在所有测试方法前执行
@AfterClass 该方法会在所有测试方法后执行
@BeforeClass和@AfterClass 的方法必须是静态方法
xxx.java-----编译------>xxx.class---加载-->方法区
class文件是JVM用到才会加载,而且只需要加载一次就够了!!!
当字节码文件加载到内存时,JVM会为他创建一个Class对象,用来保存字节码文件中的所有信息
在程序运行期间,获取Class对象,从而建剖它,取出其中各种成员(构造,方法,属性),从而使用它们
a.使用反射开发IDE(如:IDEA EClipse等)
b.使用反射开发框架(如:SSM)
一个.Class文件----->Class对象
一个构造方法---------->Constructor对象
一个成员方法---------->Method对象
一个成员变量--------->Field对象
newInstance--------->创建对象
invoke ---------->调用/执行
体验反射语法
以前创建对象:new 构造方法(参数);
反射创建对象:构造方法.newInstance(参数);
以前调用方法:对象名.成员方法(参数)
反射调用方法:成员方法.invoke(对象名,参数)
以前访问属性:对象名.成员变量名 = 值
反射访问属性:成员变量.set(对象名,值)
a.通过类名.class静态属性获取Class对象 如:Class clazz = Pig.class;
b.通过对象名.getClass获取Class对象
Pig pp = new Pig(); Class clazz = pp.getClass
c.通过Class类的静态方法
Class clazz2 = Class.forName("com.itheima.demo03_refelect.Pig")
public String getName();获取Class对象代表那个类的全限定名字
//com.itheima.demo03_refelect.Pig
public String getSimpleName();//Pig
public Object newInstance();//创建Class对象代表那个类的对象(必须保证有无惨构造)
public Constructor getConstructor(数据类型.class,数据类型.class);要求是"public"
public Constructor getDeclaredConstructor(数据类型.class,数据类型.class); 任意修饰符
public Constructor[] getConstructor();获取所有public的构造
public Constructor[] getDeclaredConstructor();获取所有任意类型的构造
如何使用构造方法创建对象
public Object newInstance(具体的参数);
问题:如果是私有构造怎么使用
public void setAccessible(true);
public Object newInstance(具体的参数);
获取成员方法一共四个方法
public Method getMethod(String methodName,数据类型.class,...)获取单个"public"
public Method getDeclaredMethod(String methodName,数据类型.class,...) 单个任意
public Method[] getMethod(String methodName,数据类型.class,...)
//所有public(包括父类继承的)
public Method[] getDeclaredMethod(String methodName,数据类型.class,...)
//所有任意修饰符的成员方法(不包括父类的)
如何调用成员方法
public Object invoke(对象名,方法的实际参数);调用方法,返回值Object就是方法的返回值
问题:如果是私有方法怎么使用
public void setAccessible(true);"设置暴力访问权限"
public Object invoke(对象名,方法的实际参数);调用方法,返回值Object就是方法的返回值
public Field getField(String filedName);获取"public"成员变量
public Field getDeclaredField(String filedName)任意
public Field[] getField(String filedName);获取所有"public"成员变量
public Field[] getDeclaredField(String filedName)获取所有任意
public set(对象名,参数);
如果私有
public void setAccessible(true);"设置暴力访问权限"
public set(对象名,参数);
注解实际上就是一个标记
主要是给编译器JVM看的
a.编译时检查
如:@Override,@FunctionalInterface
b.给框架做配置文件
@Override 检查方法是否重写正确
@FunctionalInterface 检查接口是否只有一个抽象方法
@Deprecated 过时注解 检查一个方法是否已过时
关键字:"@interface"
格式:
public @interface 注解名字{
}
使用:类上,接口上,方法上,成员变量上,局部变量上,构造方法,参数上
public @interface 注解名字{
//属性,只能添加属性
数据类型 属性名();//相当于类中 int age
数据类型 属性名() default 默认值;相当于类中 int age = 10;
}
注解中的属性并不是任何类型都可以的
a.八大基本类型和String*********
b.Class类型,枚举类型,注解类型
c.以上类型的一维数组
有属性的注解,使用时必须给属性赋值(有默认值的属性可以赋值也可以不赋值)
a.如果只有一个属性,并且名字叫value,那么赋值是value可以省略,直接写值即可
b.如果有多个属性,其中一个叫value,其他属性都有默认值时,那么value可以省略,直接写即可
总结:如果使用注解时,只需要赋值一个属性,并且该属性叫value,那么赋值时value可以省略,直接写值 即可
@Target 元注解
作用:"规定我们的注解使用对象"
元注解@Target的使用
@Target(vlaue = {}),数组中可以有以下值
必须使用ElementType.xxx,这里xxx可以是一下值
'TYPE: 用在类,接口上
'FIELD:用在成员变量上
'METHOD: 用在方法上
'PARAMETER:用在参数上
'CONSTRUCTOR:用在构造方法上
'LOCAL_VARIABLE:用在局部变量上
@Rentention 作用:"规定我们注解的生命周期"
@Retention(calue = RentensionPolicy) RentensionPolicy表示生命周期政策
必须使用RentensionPolicy.xxx xxx可以是一下值
SOURCE:注解只存在于Java源代码中,编译生成的字节码文件中就不存在了。
CLASS:注解存在于Java源代码、编译以后的字节码文件中,运行的时候内存中没有,默认值。 RUNTIME:注解存在于Java源代码中、编译以后的字节码文件中、运行时内存中,程序可以通过 反射获取该 注解
从指定位置获取注解,并取出解析中属性值
通过Java技术获取注解数据的过程则称为注解解析
了解两个接口:
Anontation:所有注解类型的公共接口,类似所有类的父类是Object
AnnotatedElement:所有注解可以作用的目标都会实现该接口
定义了与注解解析相关的方法,常用方法以下几个:
boolean isAnnotationPresent(Class annotationClass); 判断当前对象是否有指定的注 解,有则返回 true,否则返回false。
T getAnnotation(Class<T> annotationClass); 获得当前对象上指定的注解对象。 Annotation[] getAnnotations(); 获得当前对象及其从父类上继承的所有的注解对象。
a.获取注解所在的类的Class对象
b.获取注解所在的目标对象(Constructor对象,Method对象,Field对象)
c.判断注解是否存在,如果存在可以取出该注解
d.从注解中获取属性值