文章目录
〇. 基础知识
1. 数据类型
2. Switch用法
Java 5 以前:switch(expr)中,expr 只能是 byte、short、char、int
从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型
从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的
3. 类型赋值注意事项
eg1:
float f=3.4 编译错误;3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F。
eg2:
short s1 = 1; 可以;
s1 = s1 + 1; 错误;
由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
short s1 = 1; 可以
s1 += 1; 可以
因为 s1+= 1;相当于 s1 = (short(s1 + 1));其中有隐含的强制类型转换。
4. 运算符
&和&&的区别
&运算符有两种用法:(1)按位与;(2)逻辑与
&&运算符是短路与运算
逻辑或运算符(|)和短路或运算符(||)的差别也是如此
一. 继承_重写_this/super_抽象类
1. 继承_初始化
- 先初始化父类,再初始化子类
- 父类的私有成员(成员属性、成员方法)、构造函数不允许子类继承
- 子类构造方法的编写原则:要参考父类的构造方法,最好提供跟父类一样的构造方法,并且为父类构造方法传递参数,也就是说父类有几个构造方法,子类也要提供几个相对应的(参数列表也要一致)构造方法,并且子类的构造方法最好使用super关键字调用父类的构造方法,即父类有的子类一定要有,子类还可以比父类多一些构造方法
class Person{
private String name ;
public Person(){
}
public Person(String n){
name = n;
}
}
class Student extends Person{//子类也同样提供两个相同格式的构造方法
public Student(){
//调用父类"无参构造"
}
public Student(String n){
super(n);//显示调用父类"带参构造",也可以不写
}
}
public class Demo {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu = new Student("aaa");
}
}
注意: 子类的构造方法中默认使用super()调用父类的无参构造方法,可以显示的使用super([实参])调用父类的某个构造方法,super([实参])的调用必须放在这个子类构造方法的第一行有效代码。
this()和super()的调用不能在一个构造方法中同时存在,因为都需要在第一行。
- 练习1
class Person{
public Person(){
System.out.println("a");
}
}
class Student extends Person{
public Student(){
System.out.println("b");
}
public Student(String n){
System.out.println("c");
}
}
main(){
new Student();//ab
new Student("a");//ac,默认调用父类无参构造方法
}
- 练习2
class Person{
public Person(){
System.out.println("a");
}
public Person(String n){
System.out.println("b");
}
}
class Student extends Person{
public Student(){
System.out.println("c");
}
public Student(String n){
System.out.println("d");
}
}
main(){
new Student();//ac
new Student("a");//ad,由于没有指定super关键字调用父类的有参构造器,所以默认调用父类无参构造器
}
- 练习3:继承下初始化过程
class A{
public A(){
System.out.println("a");
}
}
class B{
public B(){
System.out.println("b");
}
}
==============================================
class Fu{
A a = new A();
public Fu(){
System.out.println("c");
}
}
class Zi extends Fu{
B b = new B();
public Zi(){
System.out.println("d");
}
}
main(){
new Zi();//acbd,先父后子
}
- 练习4
class Fu{
int num = 10;
public Fu(){
show();
}
public void show(){
System.out.println(num);
}
}
class Zi extends Fu{
int num = 20;
public void show(){
System.out.println(num);
}
}
main(){
Zi z = new Zi();//0
z.show();//20
}
当我们创建一个子类对象时,JVM会先创建一个父类对象;
1.子类成员属性的默认初始化;
2.父类成员属性默认初始化;
3.父类成员属性显示初始化;
4.父类构造方法;
5.子类成员属性的显示初始化;
6.子类的构造方法;
2. 继承_覆盖
-
子类重写的方法的访问修饰符要跟父类方法的"相同"或者比父类的"更宽"
从宽到窄:public、protected、(默认)、private -
重写的注意事项:
- 父类的私有方法不能被重写;因为私有的不能被继承,所以谈不上重写
- 静态方法不能被重写
-
练习
class Fu{
public void show(){
}
}
class Zi extends Fu{
@Override
void show(){//错误的,比父类访问修饰符更低了
}
}
3. this关键字
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
- this用来访问本类的成员属性、成员方法、构造方法
- this的穿透性:this实际上是从本类开始找,如果本类没有会去到父类中找,如果父类没有会去到父类的父类中找,直到找到最根类
- 练习
class Fu{
int money = 1000;
}
class Zi extends Fu{
public void show(){
System.out.println("我有:" + money + " 元钱");//1000
System.out.println("我有:" + this.money + " 元钱");//1000,体现了this的穿透性
System.out.println("我有:" + super.money + " 元钱");//1000
}
}
main(){
new Zi().show();
}
4. super关键字
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
- 用来访问父类的成员属性、成员方法、构造方法
- super的穿透性:super实际上就是从父类开始找,如果父类没有会去到父类的父类中找,直到找到最根类
- 练习
class Ye{
int n = 10;
}
class Fu extends Ye{
}
class Zi extends Fu{
int n = 1000;
public void show(){
System.out.println("super.n = " + super.n);//10
}
}
二. 接口_多态
1. 接口
- 接口使用关键字interface,会被编译成.class文件
- 接口可以被多实现,接口可以多继承接口
- 接口中很少使用成员变量,但都被隐式的定义为public static final,这三个关键字都可省略,编译时编译器会自动加上
- 接口中常用抽象方法,必须是public abstract的方法,不写也没事,编译器在编译时会自动加上,需注意继承问题
- 练习
interface IA{
void show();
}
class SubA implements IA{
void show(){//接口中的方法隐式的是:public,子类必须使用public重写
System.out.println("a");
}
}
main(){
new SubA().show();
}
1.编译错误 【选择】
2.运行时异常
3.打印:a
4.正常运行,但无任何输出;
2. 多态
- 多态是指父类的引用指向了它的某个子类对象,前提要存在继承关系
- 多态中成员访问的特点
- 访问的成员在父类中必须有,不能访问子类的特有成员(父类没有)
- 不覆盖的情况下,访问的都是父类的成员(包括属性、方法)
- 覆盖的情况下,访问父类的成员属性,子类的方法
interface IA{
public void show();
}
class Fu{
public void show2(){
System.out.println("m");
}
}
class SubA extends Fu implements IA{
public void show(){
System.out.println("a");
}
}
main(){
IA a = new SubA();//多态
a.show();//a IA里有show方法且有覆盖,运行子类的方法
a.show2();//IA里没有show2方法编译错误,IA中没有show2()
Fu f = new SubA();//多态
f.show();//编译错误,Fu中没有show()
f.show2();//m OK的,Fu中有show2方法,但没有覆盖,所以执行父类的方法
}
- 向下转型
多态不能访问子类特有成员,但是可以先将父类类型的变量,转换为它所指向的那个子类类型的,就可以访问了
注意: 向下转型时,可能出现ClassCaseException,不安全,我们可以在强转之前,对变量进行判断,然后再转换。
判断使用:instanceof运算符
格式:
变量名 instanceof 类名
判断方式:
判断左边的"变量名"是否可以转换为右边的"类名"的类型,如果是;返回true,表示可以强转,否则:返回false
Person p = new Student();
if(p instanceof Student){//true 提高程序的"健壮性",这是一个好的习惯。
Student stu = (Student)p;
}
三. static_final_包_访问修饰符_内部类
1. static关键字
- static修饰成员变量,只有一个存储空间,放在方法区的静态存储区,被多个对象共享
- static修饰成员方法,可作为工具方法,可通过类名直接调用
- 可以通过对象访问静态成员,也可通过类名直接访问静态成员(成员属性和方法)
- static成员的初始化:
- 第一次使用静态成员所在类时,在创建对象之前
- 因为静态成员早于非静态成员初始化,所以静态成员只能访问静态成员,但非静态成员可以访问非静态成员和静态成员
- 在静态方法内不能使用this/super关键字,因为在静态成员初始化时,还没产生对象
- 按照静态成员的定义顺序依次初始化
- 代码块
- 构造代码块
- 很少使用,每次创建对象时执行一次,优先于构造方法执行
- 静态代码块
- 用于初始化静态变量,第一次使用所在类时(创建对象,访问静态成员)执行一次,后续就不会执行了
- 执行顺序:静态代码块->构造代码块->构造方法
- 构造代码块
- 练习
class A{
public A(){
System.out.println("a");
}
}
class B{
public B(){
System.out.println("b");
}
}
class Fu{
public static A a = new A();
static{
System.out.println("c");
}
{
System.out.println("d");
}
public Fu(){
System.out.println("e");
}
}
class Zi extends Fu{
public static B b = new B();
static{
System.out.println("f");
}
{
System.out.println("g");
}
public Zi(){
System.out.println("h");
}
}
main(){
new Zi();//acbfdegh
}
子父类的静态成员的初始化顺序:
1.父类静态(静态成员默认初始化,再按照定义顺序执行)
2.子类静态
3.父类普通
4.子类普通
2. final关键字
- 修饰类表示不能被继承;修饰方法表示不能被重写;修饰变量表示常数,只能被赋值一次,基本数据类型则值不能被修改,引用数据类型则引用不能被修改
final finally finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表
示该变量是一个常量不能被重新赋值。 - finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。 - finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的
最后判断。
3. 包
- 定义:package xx.yy.zz.mm
- 包的层次:一般至少要三层包以上
第一层:com/cn -->国际顶级域名
第二层:公司名 --> itheima
第三层:项目名 --> gjp
第四层:子系统名/模块名… --> 学员管理模块
4. 访问修饰符
5. 内部类
- 内部类的创建,要先创建外部类
A).Outside outer = new Outside();
Outside.Inside inner = outer.new Inside();
inner.show();
B).Outside.Inside inner2 = new Outside().new Inside();
inner2.show();
- 内部类对象可以直接访问外部类成员,包括私有成员成员
class Outside{
private int num = 10;
class Inside{
private int num = 20;
public void show(){
int num = 30;
System.out.println("inside-->show() num = " + num);//30
System.out.println("this.num = " + this.num);//20
System.out.println("Outside.this.num = " + Outside.this.num);//10
}
}
}
- 局部内部类
局部内部类访问局部变量,要求局部变量必须被定义为final
class Outside{
public void show(){
final int num = 10;
class Inside{//此类只能在方法内使用
public void method(){
//局部内部类访问局部变量,要求局部变量必须被定义为final
System.out.println("method num = " +num);
}
}
Inside inner = new Inside();
inner.method();
}
}
四. Object类_异常
1. Object类
- object类是java中所有引用类型(包括数组、自定义的类)的公共基类
- 自定义的类都隐式的继承了Object类
- java.lang包下的类属于常用类,用的时候不需要导包,其他包下的类使用时就需要导包了
==和equals
-
==基本数据类型比较的是值是否相同,引用数据类型比较的是引用是否相同
-
equals方法继承于Object类,用于对象的比较,默认情况下比较的是对象的引用,相当于==,若将其重写了则比较的是对象的内容是否相同,String对象已经重写了equals方法。当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象
-
练习
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
2. 异常
基本介绍
- 没有异常处理,程序一旦发生异常,就会结束运行;做了异常处理,即使发生了异常,程序也会继续执行下去
- 异常类层次图
- Error: 通过程序无法处理的错误,常与JVM有关,当JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止
- Exception: 程序本身可以处理的异常,比如RuntimeException 、NullPointerException等
- Throwable类的几个常见方法
1).public String getMessage():获取异常信息
2).public String toString():异常类名 + “:” + 异常信息
3).public void printStackTrace():打印堆栈异常。
异常处理
- catch处理异常
1.执行流程:只要try中出现异常,立即终止try的后续代码的执行,找到相应的catch()去执行,之后执行后续代码…
2.多个catch中的"异常类型"可以"同级别"的异常,没有前后顺序的关系。
也可以是"子父级关系",父级的异常类型必须声明在多个catch的最后
try{
//可能出现多种异常
}catch(异常类型1 变量名){
}catch(异常类型2 变量名){
}catch(异常类型3 变量名){
}....
..后续代码....
- throws异常处理
- 在方法中可以正常编写代码,然后将可能抛出的异常声明到方法定义的位置
- 可以同时声明抛出多个异常,中间用逗号隔开,而且可以是子父级关系,而且没有顺序关系
public class ArrayTools{
public static int sum(int[] arr) throws NullPointerException,
ArrayIndexOutOfBoundsException,
Exception{
int sum = 0;
for(int n : arr){
sum += n;
}
return sum;
}
}
- throw异常处理
public class ArrayTools{
public static int getMax(int[] arr){
if(arr == null){
//这时,就需要告诉调用处,这里出异常了
throw new NullPointerException("空指针异常!");//执行到这里,方法会立即返回,不会向下执行了。
}
if(arr.length == 0){
throw new ArrayIndexOutOfBoundsException("你传递了一个0长度的数组!!");
}
int max = arr[0];
for(int n : arr){
max = n > max ? n : max;
}
return max;
}
}
- try…catch…finally处理异常_过程解析
finally块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句
时,finally语句块将在方法返回之前被执行
在以下4种特殊情况下,finally块不会被执行:
- 在finally语句块中发生了异常。
- 在前面的代码中用了System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭CPU。
自定义异常
A).自定义异常类,继承自Exception或者它的某个子类
class AgeException extends Exception{//如果继承Exception,属于:非运行时异常。如果继承RuntimeException,属性运行时异常。
public AgeException(String msg){//用于指定异常消息的构造方法
super(msg);
}
}
B).使用自定义异常:
public class Student {
private int age;
public void setAge(int age) throws AgeException {
if(age < 15 || age > 50){
throw new AgeException("年龄要在15到50之间!");
}
this.age = age;
}
}
C).前端调用处:
Student stu = new Student();
try {
stu.setAge(20);
} catch (AgeException e) {
System.out.println("异常了,异常信息是:" + e.getMessage());
}
五. 工具类_正则表达式
1. Date类
- 构造方法
- Date date = new Date();使用当前系统时间来构造一个Date对象;
- Date date2 = new Date(1000 * 60);使用一个毫秒值来构造一个Date对象;
- 成员方法
- getTime():获取从1970-01-01 00:00:00(在中国是08:00:00)到当前Date对象所表示时间的一个总的"毫秒值";
- setTime(long t):设置当前Date对象为t毫秒(从0毫秒到t毫秒的时间)
2. DateFormat类
- String转Date类型
SimpleDataFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String dateStr = "1992-01-01";
//转换
Date date = sdf.parse(dateStr);//parse()方法要抛出异常
- Date类型转String类型
Date date = new Date();
//转换为我们想要的格式:yyyy年MM月dd日 HH:mm:ss
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String str = sdf.format(date);
3. Calendar类
- 获取对象
- public static Calendar getInstance();用Calendar内部的静态方法获取
- 常用方法
获取某个字段(年、月、日等)的值
public int get(int field) :
int year = gc.get(Calendar.YEAR);
int month = gc.get(Calendar.MONTH) + 1;//内部月0--11,表示:1--12的月份
int day = gc.get(Calendar.DAY_OF_MONTH);
设置某个字段为value
public void set(int field,int value) : 设置某个字段为value.
//将年设置为2018
gc.set(Calendar.YEAR, 2019);
4. Math类
- 这个类没有构造方法,全部都是"静态方法",直接通过类名就可以访问
- 常用方法
1).public static int abs(int a):返回 int 值的绝对值
2).public static double ceil(double a):向上取整(返回最小的(最接近负无穷大)double 值,该值大于等于参数,并等于某个整数。)
3).public static double floor(double a):向下取整。(返回最大的(最接近正无穷大)double 值,该值小于等于参数,并等于某个整数)
4).public static int max(int a, int b):求两个int值的最大值
5).public static int min(int a, int b): 求两个int值的最小值
6).public static double pow(double a,double b):返回a的b次幂的值
7).public static double random():返回带正号的 double 值,该值大于等于 0.0 且小于 1.0。返回值是一个伪随机选择的数,在该范围内(近似)均匀分布
8).public static long round(double a):返回最接近参数的 long。结果将舍入为整数:加上 1/2,对结果调用 floor 并将所得结果强制转换为 long 类型
5. System类
- 此类没有构造方法,全部都是静态方法。
- 常用方法
1).public static void arraycopy(Object src,//源数组
int srcPos,//源数组的起始位置
Object dest,//目标数组
int destPos,//目标数组的起始位置
int length //复制长度) 【重点掌握】: 复制数组,会把源数组的全部/部分元素复制到新数组中。
int[] arr = {1,2,3,4};
int[] arr2 = new int[arr.length];
System.arraycopy(arr,0,arr2,0,arr.length);
for(int i = 0;i < arr2.length ; i++){
System.out.println(arr2[i]);
}
2).public static long currentTimeMillis()【重点掌握】:返回当前时间的毫秒值。
long time = new Date().getTime();
或者
System.currentTimeMillis();
3).public static void exit(int status):【了解】:结束虚拟机
4).public static void gc():【了解】:运行垃圾回收器。
Object-->finalize()方法:垃圾回收器在回收这个对象之前,会调用的一个方法,此方法执行完毕后,垃圾回收器会立即清除掉这个对象。
5).public static String getProperty(String key)【了解】:获取系统属性。
System.out.println("Java 运行时环境版本:" + System.getProperty("java.version"));
System.out.println("Java 运行时环境供应商:" + System.getProperty("java.vendor"));
System.out.println("Java 安装目录:" + System.getProperty("java.home"));
System.out.println("操作系统的名称:" + System.getProperty("os.name"));
关于key的值,参考:getProperties()方法
6. 包装类
自动装箱与自动拆箱
自动装箱:将基本类型转换为对应的包装类型
Integer intObj = 10;//编译后:Integer intObj = Integer.valueOf(10)
自动拆箱:将包装类型转换为对应的基本类型:
int value = intObj + 1;//编译后:int value = intObj.intValue() + 1;
- 建议:以后我们的Java中的所有的成员属性的数据类型,建议使用:包装类型
- 练习
如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象
Integer intObj1 = 100;
Integer intObj2 = 100;
System.out.println(intObj1 == intObj2);//true
Integer intObj3 = 200;
Integer intObj4 = 200;
System.out.println(intObj3 == intObj4);//false
Integer intObj5 = 100;
Integer intObj6 = new Integer(100);
System.out.println(intObj5 == intObj6);//false
System.out.println(intObj5.equals(intObj6));//true
Integer intObj7 = new Integer(100);
Integer intObj8 = new Integer(100);
System.out.println(intObj7 == intObj8);//false
System.out.println(intObj7.equals(intObj8));//true
7. 正则表达式
- 正则前最好以/^开始,以$/结束,有时候不然不识别这个正则字符串
字符类
1.多选一:[xyz]:
例:字符串中是否以h开头,以d结尾,而且中间只有一个字符,而且是元音字母a、e、i、o、u?
String s = "had";//验证这个字符串,是否符合上面的规则
String regex = "h[aeiou]d";//正则表达式,描述上面的规则的
System.out.println("1." + s.matches(regex));
例:以b 或 c 或 d 开头,中间字符是 a,并且以 d 或 t 结尾的字符串;
s = "bad";
regex = "[bcd]a[dt]";
System.out.println("2." + s.matches(regex));
2.范围:[a-z][0-9][A-Z]
例:字符串中是否以h开头,以d结尾,而且中间只有一个小写英文字母?
s = "ha 9d";
regex = "h[A-Za-z0-9]d";
System.out.println("3." + s.matches(regex));
例:字符串是否以大写英文字母开头,后跟id?
s = "Hid";
regex = "[A-Z]id";
System.out.println("4." + s.matches(regex));
3.非:[^x]
例:字符串首字母是否非数字,后跟id?
s = "_id";
regex = "[^0-9]id";
System.out.println("5." + s.matches(regex));
逻辑运算符
1.&& : 与(并且)
2.|: 或(或者)
例:
1.判断小写辅音字母:
String s = "u";
String regex = "[a-z&&[^aeiou]]";
System.out.println("1." + s.matches(regex));
2.判断首字母为h 或 H ,后跟一个元音字母,并以 d 结尾:
s = "hdd";
regex = "[h|H][a|e|i|o|u]d";//等同于:[hH][aeiou]d
System.out.println("2." + s.matches(regex));
预定义字符类
1.".":匹配任何字符;
2."\d":匹配任何的数字。相当于[0-9];
3."\D":匹配任何的非数字。相当于[^0-9];
4."\s":匹配任何空白字符。空格、制表符(\t)、换行符(\n)…
5."\S":匹配任何非空白字符。是[^\s]的简写形式;
6."\w":任何大写、小写字母,或数字或下划线。是[a-zA-Z_0-9]的简写形式;
7."\W":任何非单词字符。是[^\w]的简写形式
例:
1.判断是否是三位数字:
String s = "1234";
String regex = "\\d\\d\\d";
System.out.println("1." + s.matches(regex));
2.判断字符串是否以h开头,中间是任何字符,并以d结尾:
s = "h d";
regex = "h.d";
System.out.println("2." + s.matches(regex));
3.判断字符串是否是”had.”:
s = "had.";
regex = "had\\.";
System.out.println("3." + s.matches(regex));
4.判断手机号码(1开头,第二位是:3或5或8,后跟9位数字):
s = "15513151135";
regex = "1[358]\\d\\d\\d\\d\\d\\d\\d\\d\\d";
System.out.println("4." + s.matches(regex));
限定符
1."+" : 1次或多次;
2."?" : 0次或1次;
3."*" : 0次或多次;
4.{n} : 恰好n次;
5.{n,}: 至少n次;
6.{n,m}:n(包含)--m(包含)次;
例:
1.判断1位或多位数字:
String s = "1234324324843920853920584";
String regex = "\\d+";
System.out.println("1." + s.matches(regex));
2.判断手机号码:
s = "15513151135";
regex = "1[358]\\d{9}";
System.out.println("2." + s.matches(regex));
3.判断小数(小数点最多1次)
s = "3.3";
regex = "\\d+\\.?\\d*";
System.out.println("3." + s.matches(regex));
4.判断数字(可出现或不出现小数部分):
s = "3";
regex = "\\d+(\\.?\\d*)?";
System.out.println("4." + s.matches(regex));
5.判断一个QQ号码(5-15位数字,不能以0开头):
s = "1595959559";
regex = "[1-9]\\d{4,14}";
System.out.println("5." + s.matches(regex));
分组
String s = "DB8FV-B9TKY-FRT9J-6CRCC-XPQ4G";
String regex = "([A-Z0-9]{5}-){4}[A-Z0-9]{5}";
System.out.println(s.matches(regex));
String的split方法
String str = "2017-01-02";
String[] strArray = str.split("-");
str = "2017-----01--02----03";
strArray = str.split("[-]+");
正则表达式_String的replaceAll(String regex,String str):用str替换本字符串中所有符合regex正则的子字符串
String str = "南方近段时间奥巴马就放掉奥吧马加法do微积分IE噢吧吗加法IE我房间诶我奥巴马";
//将字符串中的"奥巴马/奥吧马/噢吧吗"替换为"*"
str = str.replaceAll("奥巴马|奥吧马|噢吧吗","*");
System.out.println(str);
str = "jfdksj2432432fjdkslj43243jfd3sljf4234324jfkdsljf42432432";
//将字符串中所有的一组连续的数字替换为一个*号
str = str.replaceAll("\\d+", "*");
System.out.println(str);
六. 迭代器_泛型
1. 迭代器
- 迭代器:用于遍历元素的对象
- Collection–>iterator()
- 练习1:基本使用
Collection<String> strList = new ArrayList<>();
strList.add("孙悟空");
strList.add("猪八戒");
strList.add("白骨精");
strList.add("牛魔王");
//获取迭代器
Iterator<String> it = strList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
- 练习2:一次获取迭代器,只能使用一次
Iterator<String> it = strList.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
while(it.hasNext()){//此循环不会被执行
System.out.println(it.next());
}
it = strList.iterator();//可以再次获取一个迭代器对象
while(it.hasNext()){//OK的
System.out.println(it.next());
}
迭代器的并发修改异常
- 练习3:使用迭代器遍历集合删除指定元素时会抛出异常
Collection<String> strList = new ArrayList<>();
strList.add("孙悟空");
strList.add("猪八戒");
strList.add("白骨精");
strList.add("牛魔王");
//获取迭代器
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String s = it.next();
if("牛魔王".equals(s)){
strList.remove(s);//会导致运行时抛出"并发修改异常":ConcurrentModificationException
}
System.out.println(s);
}
并发修改异常产生的原因: 当通过"迭代器"遍历时,通过"集合对象"去修改元素的数量,会导致"迭代器"抛出并发修改异常
解决方式:
通过迭代器遍历,就通过迭代器修改,迭代器允许调用者在迭代过程中移除元素
...
while(it.hasNext()){
String s = it.next();
if("牛魔王".equals(s)){
// strList.remove(s);//会导致运行时抛出"并发修改异常":ConcurrentModificationException
it.remove();//这就没问题了
}
System.out.println(s);
}
并不是每个元素都抛异常,比如:删除"白骨精"时就不会抛异常,原因由于next()方法的中的判断机制导致,这是一种"数值"上的巧合。 并不意味着这种操作是正确的。
2. 泛型
Collection<String> strList = new ArrayList<String>();//JDK7以前,必须这样写
JDK7以后,可以:
Collection<String> strList = new ArrayList<>();
或者
Collection<String> strList = new ArrayList();//编译通过,但会有警告。
- 泛型必须是"引用类型",不能是基本类型
- 泛型只存在于编译期,编译后,在class文件中是没有"泛型"的
泛型类的定义
public class Student<T>{
public void show(T t){
}
}
- 类名后<T>就是"泛型定义":它表示这个类可以支持泛型,构造的时候可以指定泛型
- 可以同时定义多个泛型名称,中间用逗号隔开;
public class Student<T,K,V>{
}
注意:构造此类对象时,可以不指定具体类型,但如果指定,必须三个全部指定
泛型方法的定义
public void Student{
public <T> ArrayList<T> show(T s1 ,T s2,T s3,T s4,T s5){//定义了一个支持泛型的方法
ArrayList<T> list = new ArrayList<>();
list.add(s1);
list.add(s2);
list.add(s3);
list.add(s4);
list.add(s5);
return list;
}
}
使用:
public static void main(String[] args) {
Student stu = new Student();
ArrayList<String> strList = stu.<String>show("a","b","c","c","aa");
ArrayList<Integer> intList = stu.<Integer>show(10,20,30,40,50);
}
泛型接口的定义
interface IA<T>{
public void show(T t);
}
子类实现具有泛型接口的时候,怎样处理泛型部分【必须掌握】
子类:
1.可以丢弃泛型【以后会看到这种情况】
class SubClass implements IA{
@Override
public void show(Object t) {//方法中使用泛型的位置全部变为Object类型
}
}
2.可以指定为某个具体类型【以后会看到这种情况】
class SubClass2 implements IA<String>{//指定为String类型
@Override
public void show(String t) {//重写时,必须是String类型
}
}
3.继续继承泛型【最常用】
class SubClass3<T> implements IA<T>{
@Override
public void show(T t) {
}
}
泛型通配符
1).<?> : 统配任何类型:具有任何类型泛型的集合
class Demo{
public static void main(String[] args){
}
public static void show(ArrayList<?> list){//表示:可以接收具有任何泛型的ArrayList引用
}
}
2).<? extends E>:通配所有的E类型以及E的所有子类类型的泛型
public static void main(String[] args){
ArrayList<String> strList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();
ArrayList list = new ArrayList();
ArrayList<Object> objList = new ArrayList<>();
show(strList);//OK的
show(intList);//OK的
show(list); //OK的
show(objList);//OK的
}
public static void show(ArrayList<? extends Object> list){//注意:如果写为show(ArrayList<Object> objList)错误的
}
3).<? super E>:通配所有的E类型以及E的父类的类型的泛型;
class A{}
class B extends A{}
class B1 extends B{}
class B2 extends B{}
...
main(){
ArrayList<A> list1 = new ArrayList<>();
ArrayList<B> list2 = new ArrayList<>();
ArrayList<B1> list3 = new ArrayList<>();
ArrayList<B2> list4 = new ArrayList<>();
show(list1);//Ok的
show(list2);//OK的
show(list3);//OK的
show(list4);//错误
}
public static void show(ArrayList<? super B1> list){
}
七. 集合_Linklist_Hashset
1. 集合架构
常用的集合如下:
1).Collection(接口):单列集合
|--List(接口):特点:1.有序的;2.可以存储重复元素
|--ArrayList(子类):Object数组结构,支持动态扩容,每次增加50%;
|--LinkedList(子类):双向链表结构;
|--Vector(子类):Object数组结构,支持动态扩容,每次增加1倍【线程安全】;
|--Set(接口):特点:不能存储重复元素
|--HashSet(子类):无序,唯一,基于HashMap 实现的,HashSet的值存放于HashMap的key上,对HashSet的操作底层基本都使用的HashMap的方法完成的;
|--LinkedHashSet(子类):链表、哈希表结构,特例,是有序的,通过 LinkedHashMap 来实现
|--TreeSet(子类):有序,唯一,红黑树(自平衡的排序二叉树)
2).Map(接口):双列集合--数据结构全部是应用在"键"
|--HashMap(子类):jdk1.8之前采用数组+哈希表,1.8之后采用数组+哈希表/红黑树(当链表长度大于8时转为红黑树)
|--LinkedHashMap(子类):有序,与其父类HashMap结构相同,不过在此处上又增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序,同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
|--HashTable(子类):数组+链表,【线程安全】
|--ConcurrentHashMap(子类):Node 数组+链表/红黑树,【线程安全】
|--AbstractMap
|--TreeMap(子类): 红黑树(自平衡的排序二叉树)
- Collection接口中contains方法判断元素是否存在是根据equals方法判断的
2. List集合
- List(接口):特点:1.有序的;2.可以存储重复元素;
Collection(接口):
|--List(接口):特点:1.有序的;2.可以存储重复元素;
特有方法:
1).public void add(int index,Object element):在index位置插入元素element,原index及其后续元素依次后移。
2).public int indexOf(Object o):返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1
3).public ListIterator<E> listIterator():返回"列表迭代器",双向迭代器,它是Iterator的子接口。
4).public Object remove(int index):移除index位置上的元素;
5).public E set(int index,E element):将index位置上的元素替换为element,返回值:被替换的元素;
6).public List<E> subList(int fromIndex,int toIndex):截取集合,从fromIndex开始,到toIndex - 1 的位置
7).public E get(int index) : 获取index位置上的元素;
ArrayList优缺点:
ArrayList的优点如下:
- ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
- 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
多线程场景下如何使用 ArrayList
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}
3. Set集合
- Set集合的特点:1).无序的;2).不能存储重复元素;
- 判断元素的唯一性
- 先判断hashCode(),然后再判断equals()
- 要想使用HashSet存储自定义对象,需要对自定义对象重写:hashCode()和equals()方法
4. 随机访问
如果一个数据集合实现了RandomAccess 接口,就意味着它支持随机访问,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持随机访问,如LinkedList。
推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach(内部也使用了Iterator ) 遍历。
- LinkedHashSet:它是Set的特例,它是有序的Set
八. Map_可变参数_Collections工具类
1. Map
Map架构
- Map的子类:
|–HashMap(子类,哈希表结构,"键"不能重复,但是无序的):
|–LinkedHashMap(子类,链表、哈希表结构,"键"不能重复,但是有序的) - Map的本质:Map<Object,Object>,所以还可以使用对象做键
使用自定义对象做键,要重写hashCode和equals方法。 - 常用方法
1).public V put(K key,V value):将指定的键值对存储到集合中;如果存储新的键值对返回:null;如果存储已存在的键时,用
新值替换原值,并将原值返回。
2).public V remove(Object key) : 移除key所对应的键值对。返回:被删除的value值。
3).public V get(Object key) : 用键获取对应的值。如果"键"不存在,返回null。
4).public int size() : 获取集合大小。
5).public boolean containsKey(Object key) : 判断某个键是否存在
6).public boolean containsValue(Object value) : 判断某个值是否存在
7).public Set<K> keySet() : 获取所有的"键"的Set集合
8).public Collection<V> values() : 获取所有的"值"的集合
9).public Set<Map.Entry<K,V>> entrySet() : 返回的是具有Map.Entry泛型的Set集合。此集合中存储的是若干的Entry对象,
每个Entry对象中存储一对的键值对。
HashMap底层实现
- jdk1.8之前:
JDK1.8 之前 HashMap底层是数组和链表结合在一起使用,也就是链表散列,HashMap 通过 key 的 hashCode 经过hash函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。JDK1.8 之前HashMap插入数据方式是头插法(先讲原位置的数据移到后1位,再插入数据到该位置), - jdk1.8之后:
JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。1.8之后采用尾插法(直接插入到链表尾部/红黑树)
Map的遍历
- 方式一:使用键的集合遍历
Map<String,String> map = new HashMap<>();
map.put("大师兄","孙悟空");
map.put("呆子","猪八戒");
map.put("老沙","沙和尚");
map.put("师傅","唐三藏");
//遍历集合--获取键的集合
Set<String> keys = map.keySet();
for(String k : keys){
System.out.println("键:" + k + " 值:" + map.get(k));
}
- 方式二:使用键值对Entry遍历
Map<String,String> map = new HashMap<>();
map.put("大师兄","孙悟空");
map.put("呆子","猪八戒");
map.put("老沙","沙和尚");
map.put("师傅","唐三藏");
Set<Map.Entry<String,String>> keys = map.entrySet();
for(Map.Entry<String,String> k : keys){
String key = k.getKey();
String value = k.getValue();
System.out.println("键:" + key + " 值:" + value);
}
HashMap的扩容操作是怎么实现的
在jdk1.8中,HashMap 默认的初始化大小为16,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;每次扩展的时候,都是扩展2倍;扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap和HashTable的区别
- 线程安全:HashMap线程不安全,HashTable线程安全,内部方法都加有同步锁
- 效率:因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它
- Null Key支持:HashMap支持Null Key,HashTable不支持
- 底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
- 初始容量大小和每次扩充容量大小的不同:①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方
ConcurrentHashMap 和 HashTable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
- 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
ConcurrentHashMap线程安全的具体实现方式
- JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 - JDK1.8 (上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
LinkedHashMap(有序键)
- 键是链表、哈希表结构。有序的,不能存储重复键
Map<String,String> map = new LinkedHashMap<>();
map.put("it001","aaa");
map.put("it002","bbb");
map.put("it003","ccc");
map.put("it004","ddd");
Set<String> keys = map.keySet();
for(String k : keys){
System.out.println(k + "," + map.get(k));//一定是跟存入的顺序一致的。
}
2. 可变参数
- 对于一个方法,所需要的参数如果只确定数据类型,但不确定需要的数量,可以使用"可变参数"
public static int sum(int ... nums){
}
- 对于"可变参数"的形参,编译后变为:数组,所以在方法内部就是按照数组来处理。它不能跟"同样类型数组"的形参方法重载
sum(int ... nums){}
sum(int a,int b,int c){}//可以重载
sum(int a,int b,int c,int d,int e){}//可以重载
sum(int[] arr){}//不能重载,编译错误
- 对于包含"可变参数"的方法的形参列表中,"可变参数"可以和"普通参数"共存,但要求"可变参数"必须位于参数列表的末尾
sum(int ... nums,String s )//错误的
sum(int a,int b,int c,int ... nums){}//正确的
- 一个方法的形参列表中,只能有一个"可变参数",而且必须位于参数列表的末尾。
public int sum(int ... nums,String ... strs){//编译错误
}
3. Collections集合工具类
- 常用方法
1).public static void shuffle(List<?> list):对List集合的元素进行随机置换。
2).public static <T extends Comparable<? super T>> void sort(List<T> list):对List内的元素进行"升序"排序。
注意:对于要进行排序的集合中的元素,要求元素必须实现:Comparable接口,否则会抛出异常;
关于Comparable接口的比较:
String就是实现了Comparable接口
String s1 = "ab";
String s2 = "abcd";
System.out.println(s1.compareTo(s2));
九. File_递归
1.File类
构造方法
File(String pathname) 通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
例如:
File file1 = new File("d:\\bbb\\1.png");//绝对路径--带盘符的目录
File file2 = new File("1.png");//相对路径--在当前目录下开始查找
File file3 = new File("d:\\bbb\\2.png");//可以是一个文件
File file4 = new File("d:\\");//可以是一个目录
File file5 = new File("jfkdsljfdsklfjdsafjdsalfjdskajfdsaklf");//可以运行,不会抛异常,后期通过一些方法判断这个路径是否合法。
File(String parent, String child) 根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
例如:
String dir = "d:\\bbb";
String file = "1.png";
File file6 = new File(dir,file);//效果同:file1
File file7 = new File(dir + "\\" + file);
File(File parent, String child) 根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
例如:
File parentDir = new File("D:\\bbb");
File file8 = new File(parentDir,"1.png");//效果同:file6和file1
绝对路径:带盘符的一个路径;
相对路径:不带盘符的路径。会从当前的项目目录下开始找
常用方法
- 获取方法
1).public String getAbsolutePath():获取此File对象表示的绝对路径;
2).public String getName() :返回由此抽象路径名表示的文件或目录的名称。
3).public String getPath():将此抽象路径名转换为一个路径名字符串。
File file1 = new File("d:\\bbb\\1.png");
File file2 = new File("1.png");
System.out.println(file1.getPath());//d:\\bbb\\1.png--获取的就是封装File对象的目录
System.out.println(file2.getPath());//1.png--获取的就是封装File对象的目录
4).public long length() : 只能操作文件;返回由此抽象路径名表示的文件的长度。如果此路径名表示一个目录,则返回值是不确定的。
- 判断方法
1).public boolean exists() : 判断文件/目录是否存在
File file = new File("jfdksljfdskl");
System.out.println("文件或者目录是否存在:" + file.exists());//false
file = new File("d:\\bbb\\1.png");
System.out.println("文件或者目录是否存在:" + file.exists());//true
2).public boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。
File file1 = new File("D:\\bbb\\1.png");
File file2 = new File("D:\\bbb");
File file3 = new File("jfdskljfds");
System.out.println(file1.isDirectory());//false
System.out.println(file2.isDirectory());//true
System.out.println(file3.isDirectory());//false
3).public boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。
File file1 = new File("D:\\bbb\\1.png");
File file2 = new File("D:\\bbb");
File file3 = new File("jfdskljfds");
System.out.println(file1.isFile());//true
System.out.println(file2.isFile());//false
System.out.println(file3.isFile());//false
- 创建与删除
1).public boolean createNewFile() : 创建一个文件
File file1 = new File("demo05.txt");
if(!file1.exists()){
file1.createNewFile();//创建文件
}
2).public boolean mkdir() : 创建单级目录
File file1 = new File("aaa");
file1.mkdir();//创建目录
3).public boolean mkdirs(): 创建多级目录
File file1 = new File("aaa\\bbb\\ccc\\ddd");
file1.mkdirs();//创建多级目录
4).public boolean delete() : 删除文件及空目录,如果是目录,必须是"空目录";
如果是多级目录,必须一级一级的删除。
- listFiles方法,获取此目录下所有子文件和子目录的File[]数组
1).public File[] listFiles() : 获取此目录下所有子文件和子目录的File[]数组。
此方法在以下几种情况下会返回NULL:
1).当前的File对象封装的是一个"文件";
File file = new File("d:\\bbb\\1.png");
File[] fileArray = file.listFiles();
for(File f : fileArray){//抛出空指针异常
}
2).当前的File对象封装的是一个目录,但,是一个"系统目录";
File file = new File("D:\\System Volume Information");
File[] fileArray = file.listFiles();
for(File f : fileArray){//抛出空指针异常
}
3).当前的File对象封装的文件/目录不存在。
File file = new File("fjdskljfdsljf");
File[] fileArray = file.listFiles();
for(File f : fileArray){//抛出空指针异常
}
2).如果当前File对象封装的是一个"目录",但,是一个空目录,此方法不会返回空指针,而是返回的0长度数组
File file = new File("d:\\ccc");//此目录是存在的,是一个空目录
File[] fileArray = file.listFiles();//返回的是一个0长度的数组,不是空指针。
- 练习:在某个目录下查找.java文件
1.封装初始目录:
File file = new File("d:\\单级目录");
2.获取此目录下所有的子文件/子目录的File[]数组
File[] fileArray = file.listFiles();
3.遍历:
for(File f : fileArray){
//4.判断是否是文件
if(f.isFile()){
//5.判断是否是.java文件
if(f.getName().endsWith(".java")){
System.out.println(f);
}
}
}
2.递归
- 注意事项:
1).递归的层次不能太深,否则会堆栈溢出;
2).递归一定要有出口,否则就是死递归;
3).构造方法不能递归调用:
递归形式1:
class Student{
public Student(){
this();//编译错误
}
}
递归形式2:下面的代码:编译错误
class Student{
public Student(){
this("aa");
}
public Student(String s){
this();
}
}
- 练习:检索一个多级目录下的某种文件
main(){
//1.封装初始目录
File dir = new File("d:\\20170719_黑马就业班");
//递归调用
findFile(dir,".java");
}
public static void findFile(File dir,String hz){
//2.列出此目录下所有的子文件/子目录
File[] fileArray = dir.listFiles();
if(fileArray == null){
return;
}
//3.遍历
for(File f : fileArray){
//4.判断是否是文件,而且.java结尾
if(f.isFile() && f.getName().endsWith(".java")){
System.out.println(f);
}else if(f.isDirectory()){
//继续递归
findFile(f,hz);
}
}
}
十.字节流、高效流、Properties类
1. IO流架构
1).字节流:
A).输出流:OutputStream
|--FileOutputStream(基本流)
|--FilterOutputStream(不学)
|--BufferedOutputStream(缓冲流)
B).输入流:InputStream
|--FileInputStream(基本流)
|--FilterInputStream(不学)
|--BufferedInputStream(缓冲流)
2).字符流:
A).输出流:Writer
|--OutputStreamWriter(转换流)
|--FileWriter(基本流--基础班学习的)
|--BufferedWriter(缓冲流)
B).输入流:Reader
|--InputStreamReader(转换流)
|--FileReader(基本流--基础班学习的)
|--BufferedReader(缓冲流)
2. FileOutputStream基本输出流
- 构造方法
1).FileOutputStream(String name) 创建一个向具有指定名称的文件中写入数据的输出文件流。 覆盖写入
2).FileOutputStream(String name, boolean append) 创建一个向具有指定 name 的文件中写入数据的输出文件流。 追加写入
3).FileOutputStream(File file)创建一个向指定 File 对象表示的文件中写入数据的文件输出流。 覆盖写入
4).FileOutputStream(File file, boolean append) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。 追加写入
注意:构造时,文件可以不存在,会自动创建一个。
- 三种输出的方法
1).write(int b):输出一个字节;
2).write(byte[] arr):输出一个字节数组
3).write(byte[] arr,int off,int len):输出一个字节数组的一部分。
- 追加写入和换行
1).追加写入:使用两个构造方法
FileOutputStream(String name, boolean append)
FileOutputStream(File file, boolean append)
2).换行:
out.write("\r\n".getBytes());
3).关闭流:
out.close();
3. FileInputStream基本输入流
- 构造方法
FileInputStream(String name) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
FileInputStream(File file) 通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
注意:构造时,文件必须存在,否则抛异常。
- 两种读取的方法
1).int read():读取一个字节;返回值:读取的字节
2).int read(byte[] b):读取一个字节数组;返回值:读取的字节数量
- 练习1 :复制文件_一次读写一个字节
//1.构造一个输入流
FileInputStream in = new FileInputStream("d:\\douyu.exe");
//2.构造一个输出流
FileOutputStream out = new FileOutputStream("e:\\douyu.exe");
//3.复制:一次读写一个字节
int b = 0;
while((b = in.read()) != -1){
out.write(b);
}
//4.关闭资源
out.close();
in.close();
- 练习2 :复制文件_一次读写一个字节数组
//1.构造一个输入流
FileInputStream in = new FileInputStream("d:\\douyu.exe");
//2.构造一个输出流
FileOutputStream out = new FileOutputStream("e:\\douyu.exe");
//3.复制:一次读写一个字节数组
byte[] byteArray = new byte[1024];
int len = 0;
while((len = in.read(byteArray)) != -1){
out.write(byteArray,0,len);
}
//4.关闭资源
out.close();
in.close();
4. BufferedOutptuStream高效缓存输出流
- BufferedOutputStream工作方式:
1).调用write()方法时,数据被输出到"缓存区"中。
2).当缓存区满,或者flush()或者close()时,会将缓存区的内容一次性写入到磁盘 - 构造方法:
BufferedOutputStream(OutputStream out) 创建一个新的缓冲输出流,以将数据写入指定的底层输出流
5. BufferedInputStream高效缓存输入流
-
BufferedInputStream的工作方法:
1).先从文件中读取8K数据到内部缓存区;
2).再从缓存区读取到程序中(一次读取一个字节,一次读取一个字节数组) -
构造方法:
BufferedInputStream(InputStream in) 创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用 -
练习:使用高效流完成对文件的复制
//将"D:\\单级目录"复制到"E:\\"下,模拟在Windows下右键点击复制,到E:\\右键点击粘贴。
//1.封装原目录
File srcDir = new File("D:\\单级目录");
//2.封装目标目录
File destDir = new File("E:\\");
//3.判断目标目录下,是否存在原目录,如果不存在则:创建
destDir = new File(destDir,srcDir.getName());//File(File parent,String child)
if(!destDir.exists()){
//创建目录
destDir.mkdir();
}
//4.获取源目录下所有子文件的File[]数组
File[] fileArray = srcDir.listFiles();
//5.循环
for(File f : fileArray){//f = D:\\单级目录\\[Java参考文档].JDK_API_1_6_zh_CN.CHM destDir = "E:\\单级目录\\[Java参考文档].JDK_API_1_6_zh_CN.CHM"
//直接复制
BufferedInputStream in = new BufferedInputStream(new FileInputStream(f));
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(new File(destDir,f.getName())));
//一次复制一个字节数组
byte[] byteArray = new byte[1024];
int len = 0;
while((len = in.read(byteArray)) != -1){
out.write(byteArray,0,len);
}
out.close();
in.close();
}
System.out.println("目录复制完毕");`
6. 字节流与字符流的比较
- 字节流,读写的最小单位是一个字节,适合各种数据类型的文件,但不适合读写汉字等大于一个字节的文字,有可能读写到半个字符
- 字符流,读写的最小单位是一个字符,专门用于读写文字,可根据操作系统底层使用的编码表来确定几个字节为一个字符
7. 编码表
- ASCII码表:最早的美国的编码表。128个字符,一个字符用8位(1个字节)表示
- GBK:目前使用的中文码表,中文版的Windows底层使用的,前128个字符兼容ASCII码表的字符,之后全部是汉字和中文字符。一个中文使用2个字节表示 ,2万的中文和符号
- **iso-8859-1:**拉丁码表 latin,用了一个字节用的8位。1-xxxxxxx 负数。没有中文
- GB2312:简体中文码表。包含6000-7000中文和符号。用两个字节表示。两个字节都是开头为1 ,两个字节都是负数。
- unicode:国际标准码表:无论是什么文字,都用两个字节存储。
- UTF-8:基于unicode,一个字节就可以存储数据,不要用两个字节存储,而且这个码表更加的标准化,
在每一个字节头加入了编码信息(后期到api中查找)
8. OutputStreamWriter字符输出流
- 构造方法
OutputStreamWriter(OutputStream out,String charsetName)
// 使用GBK编码方式写入文件
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream("demo02.txt"),"GBK");
out.write("你好");
out.close();
9. InputStreamReader字符输入流
- 构造方法
InputStreamReader(InputStream in , String charsetName)
// 使用GBK方式读取demo02.txt中的内容
InputStreamReader in = new InputStreamReader(new FileInputStream("demo02.txt"),"GBK");
//一次读取一个字符
int c = 0;
while((c = in.read()) != -1){
System.out.println("读取的字符是:" + (char)c);
}
in.close();
- 字符流父子类的区别
父类 | 子类 |
---|---|
OutputStreamWriter | FileWriter |
InputStreamReader | FileReader |
父类:可以指定编码表 | |
子类:只能使用系统默认的编码表(GBK),子类不能指定编码表 |
10. ObjectOutputStream序列化流
-
序列化:是指可以将"一个对象,连同属性值"整体写入到一个文件中,或者通过网络传输,这个过程叫:序列化
-
反序列化:将之前序列化的对象再次读取到内存中,并且创建对象,这个过程叫:反序列化
-
构造方法
ObjectOutputStream(OutputStream out)
eg:
Student stu = new Student();
stu.name = "章子怡";
stu.age = 20;
stu.sex = '女';
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("demo04_Object.txt"));
//将stu对象存储到文件中
out.writeObject(stu);
out.close();
注意:
- 被序列化的对象(Student)必须实现:java.io.Serializable接口。
Serializable接口中没有任何方法,这种接口叫:标记接口。表示:如果某个类实现了这个接口,代表具有了某项功能。 - 序列化时可以将多个对象序列化到一个文件中,但反序列化时会出现问题,所以不建议将多个对象序列化到一个文件中。
如果想序列化多个对象,可以将多个对象存储到一个集合中,然后将集合对象序列化到文件中。
11. ObjectInputStream反序列化流
- 构造方法
ObjectInputStream(InputStream in)
eg:
//1.构造一个反序列化流
ObjectInputStream in = new ObjectInputStream(new FileInputStream("demo04_Object.txt"));
//2.反序列化
Object obj = in.readObject();//会从文件中读取Student对象,并在内存中创建一个Student对象,注意:不调用构造方法
System.out.println(obj);
Student stu = (Student)obj;
System.out.println(stu.name + "," + stu.age + "," + stu.sex);
//3.关闭
in.close();
注意:
- 反序列化会参考文件中的记录,在内存中创建一个这个类的对象,但不会调用构造方法。
- 关于"序列号":当被序列化对象实现Serializable接口后,会建议添加一个"序列号",用来管理序列化对象的版本信息。
建议:创建这个"序列号",可以很方便的管理被序列化对象。
public class Student implements Serializable{
private static final long serialVersionUID =1L;
...
}
瞬态关键字transient
如果被序列化的对象中哪些属性不希望被序列化,可以使用:transient修饰,例如:
class Student implements Serializable{
public String name;
public transient int age;//age不需要被序列化
}
12. Properties配置文件工具类
- java.util.Properties(类),它直接实现Map接口,所以它就是一个:Map集合
- 它里面包含了一些方法,可以配置"IO流",很方便的读写"配置文件"
- 读写配置文件的方法:
1).写入配置文件:store(Writer out,String comments):将集合内的所有键值对一次性的输出到文件中;第二个参数:写入到注释的
2).读取配置文件:load(Reader in):一次性的将配置文件中的所有键值对读取到集合中;
- 练习1:写入到配置文件
//1.创建一个Properties对象
Properties pro = new Properties();
//2.存储数据
pro.setProperty("listName","我的最爱");//相当于:Map的put(Object,Object)
pro.setProperty("listSize","50");
pro.setProperty("listContent","小草,义勇军进行曲");
pro.setProperty("listDate" ,"2017-08-06");
//3.写入配置文件
FileWriter out = new FileWriter("demo10.txt");
pro.store(out,null);//会将集合中所有的键值对输出到:demo10.txt中
out.close();
- 读取配置文件
//1.创建一个Properties对象
Properties pro = new Properties();
//2.读取数据
FileReader in = new FileReader("demo10.txt");
pro.load(in);//会将配置文件中的所有键值对,读取到集合中
in.close();
//3.遍历Map
Set<String> keys = pro.stringPropertyNames();//相当于Map的keySet()方法
for(String k : keys){
System.out.println("键:" + k + " 值:" + pro.getProperty(k));//getProperty相当于:Map的get()方法
}
- Propeties类常用的方法:
1).setProperty(String key,String value):相当于Map的put(Object key ,Object value)
2).getProperty(String key):相当于Map的get(Object key);
3).Set<String> stringPropertyNames():相当于Map的keySet()
十二. 反射
-
反射:也叫"反向加载",是指程序有能力在 “运行时” ,动态的加载一个"类",并创建对象,并调用成员属性和成员方法。
在编译时,可以不知道这个类的存在 -
示例
//使用反射,优点:解开两个类的紧耦合
Class c = Class.forName("cn.itheima.demo01_反射的概念.Student3");
//创建对象
Object o = c.newInstance();
//获取方法
Method m = c.getMethod("show3");
//执行方法
m.invoke(o);
1. 类的加载
- 当我们第一次使用(第一次创建对象,第一次访问它的静态成员)某个类时,JVM会去找这个类的class文件并在 “方法区” 中创建一个Class对象,然后将这个类信息写入到这个Class对象中。后期通过这个Class对象就可以获取这个类的内部信息
2. Class对象的初始化时机
- 第一次创建类的实例
Student stu = new Student();//1.创建它的Class对象;2.创建Student对象
...
Student stu2 = new Student();//1.只会创建Student对象
- 第一次使用类的静态变量,或者为静态变量赋值
System.out.println(Student.PI);//第一次访问静态属性,会创建它的Class对象
...
Student stu = new Student();//不会创建Class对象,会直接创建对象
- 第一次调用类的静态方法:
Student.show();//第一次调用静态的show()方法;会创建它的Class对象;
..
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
Class.forName(String 全名限定的类名)
- 初始化某个类的子类
class Fu{}
class Zi extends Fu{}
main(){
new Zi();//1.创建Fu的Class对象,2.创建Zi的Class对象,3.创建Fu对象,4.创建Zi对象
}
- 直接使用java.exe命令来运行某个主类
class Demo{
public static void main(String[] args){
}
}
---------------
命令行:
C:>java Demo
3. 获取Class对象的三种方式
- Object–>getClass() --被所有引用类型继承
Student stu = new Student();
Class c1 = stu.getClass();
- 任何数据类型(包含基本类型)都有一个"静态属性–class",只能通过"类型名"获取,不能通过对象获取
Class c2 = Student.class;
- 通过反射
Class c3 = Class.forName("cn.itheima.demo03_获取Class对象的三种方式.Student");
System.out.println(c1 == c2);//true
System.out.println(c2 == c3);//true
4. 获取构造方法并创建对象
1).批量获取【了解】:
1).public Constructor[] getConstructors():获取所有的公有构造方法。
2).public Constructor[] getDeclaredContructors():获取所有的构造方法,包括公有、受保护的、默认的、私有的
2).获取单个【掌握】:
1).public Constructor getConstructor(Class ... parameterTypes):获取某个公有的构造方法
2).public Constructor getDeclaredConstructor(Class ... parameterTypes):获取某个构造方法,可以是:公有、受保护、默认、私有的
调用构造方法,创建对象【重点掌握】:
1).Constructor --> newInstance(Object ... 实参列表)
注意:要调用私有构造方法,要先设置暴力访问:
Constructor --> setAccessible(true)
2).Class --> newInstance():调用这个类的公有、无参构造方法,前提是,这个类必须有这个构造方法
5. 获取成员属性并赋值和取值
1).批量获取:
1).public Field[] getFields():获取所有公有成员属性;
2).public Field[] getDeclaredFields():获取所有的成员属性。包括公有、受保护、默认、私有
2).单个获取:
1).public Field getField(String fieldName):获取某个公有的成员属性
2).public Field getDeclaredField(String fieldName):获取某个成员属性,可以是公有、受保护、默认、私有。
为属性赋值:注意:一定要先创建对象
Field --> set(Object targetObj,Object value):为targetObj对象中的Field属性赋值为value
Field --> get(Object targetObj):获取targetObj中的Field属性的值。
注意:如果是私有属性,要先设置暴力访问:Field --> setAccessible(true)
6. 获取成员方法并调用
1).批量获取:
1).public Method[] getMethods():获取所有公有成员方法;包括继承的方法
2).public Method[] getDeclaredMethods():获取所有成员方法,包括公有、受保护、默认、私有
2).单个获取:
1).public Method getMethod(String methodName,Class ... parameterTypes):获取某个公有的成员方法
2).public Method getDeclaredMethod(String methodName,Class ... ps):获取某个成员方法,包括公有、受保护、默认、私有
3).调用方法:
Method --> Object invoke(Object targetObj,Class ... parameterTypes):
调用这个Method的invoke()方法,就是去调用Method所封装的那个方法。
调用invoke()的返回值,就是调用Method所封装方法的返回值;
注意:访问私有方法,要设置暴力访问:Method --> setAccessible(true)
十三. 字符串
1.字符串
1.1 Sting、StringBuilder、StringBuffer的区别
区别:
- 可变性
三者都是使用char[]数组保存字符串,String使用final修饰char[]数组,所以不可变的,另外两个没有使用final修饰,所以是可变的 - 线程安全
String被final修饰,可以理解为一个常量,所以是线程安全的;StringBuilder与StringBuffer都继承自公共父类AbstractStringBuider,公共父类中定义了一些公共方法,用于被两个继承,而StringBuffer对其中的方法都加了同步锁,所以是线程安全的,StringBuilder没加同步锁,所以它是线程不安全的; - 性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
StringBuffffer 每次都会对 StringBuffffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用 StringBuffffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据 = String
- 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区下操作大量数据 = StringBuffffer
1.2 ==与equals的区别
- ==:
- 基本数据类型:比较的是值是否相同
- 引用数据类型:比较的是内存地址
- equals:
- 没有覆盖equals方法:通过equals比较两个对象时,等价于通过equals()比较这两个对象
- 覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)
- 说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - 练习
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
2. 工作中遇到的问题
2.1 去除字符串中不可见字符
String newStr = str.replaceAll("\\p{C}", "");
十四. Native关键字
1. 什么是Native Method
- 简单地讲,一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用一个C的函数。
在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。,下面给了一个示例
public class IHaveNatives
{
native public void Native1( int x ) ;
native static public long Native2() ;
native synchronized private float Native3( Object o ) ;
native void Native4( int[] ary ) throws Exception ;
}
这些方法的声明描述了一些非java代码在这些java代码里看起来像什么样子(view)
- 标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。 native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程)。
- 一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。 这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似。当一个native method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这非些基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现。有一点要牢牢记住:我们可以在一个native method的本地实现中访问所有的java特性,但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。
- native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。 需要注意当我们将一个本地方法声明为final的情况。用java实现的方法体在被编译时可能会因为内联而产生效率上的提升。但是一个native final方法是否也能获得这样的好处却是值得怀疑的,但是这只是一个代码优化方面的问题,对功能实现没有影响。
- 如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
2.为什么要使用Native Method
java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。
- 与java环境外交互:
有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。 - 与操作系统交互:
JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
3.JVM怎样使Native Method跑起来
- 我们知道,当一个类第一次被使用到时,这个类的字节码会被加载到内存,并且只会加载一次。在这个被加载的字节码的入口维护着一个该类所有方法描述符的list,这些方法描述符包含这样一些信息:方法代码存于何处,它有哪些参数,方法的描述符(public之类)等等。
- 如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才会被加载,这是通过调用java.system.loadLibrary()实现的
- 最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们再选择使用本地方法。
1、java自带的jvm调优工具VisualVM一
https://www.cnblogs.com/happy-rabbit/p/6232581.html
十五. 设计模式
1. 代理模式
代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。比如我们在租房子的时候会去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。先来看看关系图:
根据上文的阐述,代理模式就比较容易的理解了,我们看下代码:
public interface Sourceable {
public void method();
}
public class Source implements Sourceable {
@Override
public void method() {
System.out.println("the original method!");
}
}
public class Proxy implements Sourceable {
private Source source;
public Proxy(){
super();
this.source = new Source();
}
@Override
public void method() {
before();
source.method();
atfer();
}
private void atfer() {
System.out.println("after proxy!");
}
private void before() {
System.out.println("before proxy!");
}
}
测试类:
public class ProxyTest {
public static void main(String[] args) {
Sourceable source = new Proxy();
source.method();
}
}
输出:
before proxy!
the original method!
after proxy!
代理模式的应用场景:
如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:
- 修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。
- 就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。
使用代理模式,可以将功能划分的更加清晰,有助于后期维护!
2. 单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
注意: uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
3. 工厂模式
4. 策略模式
策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下:
图中ICalculator提供统一的方法,AbstractCalculator是辅助类,提供辅助方法,接下来,依次实现下每个类:
首先统一接口:
public interface ICalculator {
public int calculate(String exp);
}
辅助类:
public abstract class AbstractCalculator {
public int[] split(String exp,String opt){
String array[] = exp.split(opt);
int arrayInt[] = new int[2];
arrayInt[0] = Integer.parseInt(array[0]);
arrayInt[1] = Integer.parseInt(array[1]);
return arrayInt;
}
}
三个实现类:
public class Plus extends AbstractCalculator implements ICalculator {
@Override
public int calculate(String exp) {
int arrayInt[] = split(exp,"\\+");
return arrayInt[0]+arrayInt[1];
}
}
public class Minus extends AbstractCalculator implements ICalculator {
@Override
public int calculate(String exp) {
int arrayInt[] = split(exp,"-");
return arrayInt[0]-arrayInt[1];
}
}
public class Multiply extends AbstractCalculator implements ICalculator {
@Override
public int calculate(String exp) {
int arrayInt[] = split(exp,"\\*");
return arrayInt[0]*arrayInt[1];
}
}
简单的测试类:
public class StrategyTest {
public static void main(String[] args) {
String exp = "2+8";
ICalculator cal = new Plus();
int result = cal.calculate(exp);
System.out.println(result);
}
}
输出:10
策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。