以下内容是从任小龙讲师课堂笔记中整理。
代码块
在类或方法中,直接使用"{}"括起来的一段代码,表示一块代码区域。
代码块里变量属于局部变量,只在自己所在的区域(前后{})内有效。
根据代码块定义的位置的不同,又分成三种形式
1):局部代码块
直接定义在方法内部的代码块
一般的,我们是不会直接使用局部代码块的,只不过我们会结合if,while,for
2):初始化代码块(构造代码块):直接定义在类中。
class SuperClass{ { System.out.println("初始化代码块"); } } public class test { public static void main(String[] args){ new SuperClass(); new SuperClass(); } }
执行结果
初始化代码块
初始化代码块
反编译后的代码
import java.io.PrintStream; class SuperClass { SuperClass() { System.out.println("初始化代码块"); } } public class test { public static void main(String[] args) { new SuperClass(); new SuperClass(); } }
每次创建对象都会执行初始化代码块;
每次创建对象都会调用构造器,在构造器之前,会先执行本类中的初始化代码块。
通过反编译之后,我们发现初始化代码块也作为构造器的最初的语句。
我们一般不使用初始化代码块,结构难看。即便要做初始化操作,我们一般在构造器函数中做即可。
如果初始化操作代码较多,此时构造器结构较混乱,专门定义一个方法做初始化操作,再在构造器中调用。
3):静态代码块:使用static修饰的初始化代码块
在主方法执行前执行静态代码块,而且只执行一次。
main方法是程序的入口,为什么静态代码块优先于main方法执行。
-->静态成员随着字节码的加载也加载进JVM,此时main方法还没执行,因为方法需要JVM调用。
先把字节码加载进JVM,而后JVM再调用main方法。
一般的,我们用来做初始化操作,加载资源,加载配置文件等
class SuperClass{ { System.out.println("初始化代码块"); } static { System.out.println("静态代码块"); } } public class test { public static void main(String[] args){ new SuperClass(); new SuperClass(); } }
运行结果
静态代码块
初始化代码块
初始化代码块
Q:如下代码执行结果是什么?
class SuperClass{ SuperClass(){ System.out.println("SuperClass"); } } class SubClass extends SuperClass{ static{ System.out.println("SubClass static"); } SubClass(){ System.out.println("SubClass 构造器"); } } public class test { private static test t = new test(); private SubClass sub = null; static { System.out.println("test class static"); } public test(){ sub = new SubClass(); System.out.println("test构造器"); } public static void main(String[] args){ System.out.println("Hello"); } }
运行结果:
SubClass static
SuperClass
SubClass 构造器
test构造器
test class static
Hello
反编译后可以看出
静态字段是通过静态代码块初始化,非静态字段是通过构造器初始化
final类和final方法
为什么需要使用final修饰符
继承关系最大弊端是破坏封装:子类能够访问父类的实现细节,而且可以通过方法覆盖的形式修改实现细节。
多个修饰符之间没有先后关系
final本身的含义是"最终的,不可改变的",它可以修饰非抽象类,非抽象方法和变量。注意:构造方法不能使用final修饰,因为构造方法不能继承,肯定是最终的。
final修饰的类表示是最终的,该类不能再有子类。
只要满足以下条件就可以把一个类设计成final类:
某类不是专门为继承而设计
出于安全考虑,类的实现细节不允许改动,不准修改源代码
确信该类不会再被拓展
面试题:列举5个Java中内置的使用final修饰的类
Java里final修饰的类有很多,比如八大基本数据类型包装类和String等
final修饰的方法:最终的方法,该方法不能被子类覆盖
什么时候的方法需要使用final修饰
1):在父类中提供的统一的算法骨架,不准子类通过方法覆盖来修改,此时使用final修饰,模板方法设计模式。
2):在构造器中调用的方法(初始化方法),此时一般使用final修饰
注意:final修饰的方法可以调用,但不能覆盖。
常量的分类:
1):字面值常量,直接给出的数据值,如1,2,3
2):final修饰的变量
final修饰的变量:表示常量,只能赋值一次,不能再次赋值
final是唯一可以修饰局部变量的修饰符
1):final变量一旦赋予初始值,系统不会为final字段初始化
2):final变量一旦赋予初始值,就不能被重新赋值。
3):常量命名规范:常量名符合标识符,单词全部使用大写字母,如果是多个单词,单词使用下划线连接。
全局静态常量:public static final 修饰的变量,直接使用类名调用。
Q:final修饰的引用类型变量表示引用的地址不能改变,还是引用空间中的数据不能改变。
final修饰基本类型变量:表示该变量的值不能改变,即不能用”=”号重新赋值。
final修饰引用类型变量:表示该变量的引用的地址不能变,而不是引用地址里的内容不能变。
final是唯一可以修饰局部变量的修饰符
局部内部类只能访问final修饰的局部变量。
Public class Person { String name; } public class test { public static void main(StrIng[] args){ final Person p = new Person(); p.name = "张三"; p.name = "李四"; //下面的代码编译失败,因为p是final修饰的,p引用的地址是不能改变的 //p = new Person(); } }
p使用final修饰,故其引用地址不能改变,但是引用地址执行Person对象中name的引用地址是可以改变的
设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
其实还有两类:并发型模式和线程池模式。
单例模式:
在整个应用中某一个类只有一个实例(一个类在堆内存中只存在一个对象),即所有指向该类型实例的引用都指向同一块内存。
单例模式步骤:
1):必须在该类中,自己先创建出一个对象
2):私有化自身的构造器,防止外界通过构造器创建新的对象
3):向外暴露一个公共的静态方法用于获取自身的对象
class Single{ private static Single instance = null; private Single(){} public static Single getInstance(){ if(null == instance){ instance = new Single(); } return instance; } } public class test { public static void main(String[] args){ Single single = Single.getInstance(); } }
工具类:存放某一类事物的工具方法的类
工具类存放的包:工具包(util,utils,tool,tools,helper,helpers)
工具类名称:XxxUtil,XxxUtils/XxxTool,XxxTools,如ArrayUtil
工具类如何设计:工具在开发中其实只需要存在一份即可。
1):如果工具方法没有使用static修饰,说明工具方法得使用工具类的对象来调用,此时把工具类设计为单例的。
2):如果工具方法全部使用static修饰,说明工具方法只需要使用工具类类名调用即可,此时必须把工具类的构造器私有化(防止创建工具类对象调用静态方法)
一般首选第二种,简单。在JDK中提供的工具类都是第二种。如java.util.Arrays类
抽象类
使用abstract修饰且没有方法体的方法,称为抽象方法。
特点:
1.使用抽象abstract修饰,方法没有方法体,留给子类去实现
2.抽象方法修饰符不能是private和final以及static,为什么?
3.抽象方法必须定义在抽象类或接口中。
一般的,习惯性把abstract写在方法修饰符最前面,一看就知道是抽象方法。
使用abstract修饰的类,称为抽象类
特点:
1.不能创建实例,即不能new一个抽象类
2.可以不包含抽象方法,一旦包含,该类必须是抽象类
3.若子类没有实现父类所有的抽象方法,那么子类也得作为抽象类。
4.构造方法不能定义成私有的,否则不能创建子类对象(创建子类对象前先调用父类构造方法)
5.抽象类不能使用final修饰,因为必须有子类,抽象方法才能得以实现
6.是不完整的类,需作为基类,功能才能得以实现。
abstract class OperateTimeTemplate{ //模板方法,总体算法的骨架,子类不能修改 public final long getTotalTime() { long begin = System.currentTimeMillis(); //具体操作留给子类实现 this.doWork(); long end = System.currentTimeMillis(); return end - begin; } protected abstract void doWork(); } class StringOperateTemplate extends OperateTimeTemplate{ protected void doWork(){ String str = ""; for(int i = 0;i < 10000; i++){ str += i; } } } class IntOperateTemplate extends OperateTimeTemplate{ protected void doWork(){ int sum = 0; for(int i = 0;i < 10000; i++){ sum += 1; } } } public class test { public static void main(String[] args){ new StringOperateTemplate().getTotalTime(); new IntOperateTemplate().getTotalTime(); } }
接口
接口只定义了类应当遵循的规范,却不关心这些类的内部数据和其功能的实现细节,接口只规定了类里必须提供的方法,从而分离了规范和实现,增强了系统的可扩展性和可维护性。
面向接口编程:
接口和实现类体现了真正的多态。
Java中的接口:专家说:多个抽象类的抽象就是接口。
在java中最小的程序单元就是类,接口其实是一个特殊类。
Java中接口表示规范,用于定义一组抽象方法,表示某一类事物必须具备的功能,要求实现类必须实现该接口并提供方法实现。
定义类语法:[public] class 类名{}
定义接口语法: [public] interface 接口名{},(在这里还没有考虑接口的父接口等等)
编译之后,接口和类一样,生成一份字节码。
接口存在的成员:
1.接口中没有构造函数,不能使用new创建对象。
2.接口定义的成员变量实质是全局静态常量,默认使用public static final来修饰
public static final String NAME = “”;
3.接口中定义的方法都是公共的抽象方法(反编译确认),默认使用public abstract来修饰方法。
public abstract void work();一般的,我们在接口中定义方法,不喜欢使用修饰符。
4.接口中定义的内部类都是公共的静态内部类,默认使用public static来修饰内部类。
public static interface ABC{}
标志接口:接口中没有任何成员,就仅仅是一个接口的定义,就是一个标志,其他类实现该接口,就属于该家族,我们可以通过第三方代码赋予该接口实现类特殊功能。(不推荐)
常量接口:有人喜欢使用接口来封装多个常量信息,我们称之为常量接口,其目的和常量类相同,(不推荐)
接口的特点和接口的继承
特点:
1.没有构造方法,也不能显示定义构造器,不能实例化
2.接口只能继承接口,不能继承类,且接口支持多继承(类时单继承关系)
修饰符 interface 接口名 extends 接口1,接口2
3.接口里的方法全是抽象,默认修饰符public abstract
4.接口里的字段全部是全局静态常量,默认修饰符是public static final
5.接口里的内部类全是静态的,默认修饰符是public static
接口和接口之间只能是继承关系,使用extends来表示
接口和实现类只能是实现关系,使用implements来表示
接口的实现者:实现类
接口仅仅只是定义了某一类事物应该具有某些功能,但是没有提供任何实现
此时,我们得提供类,再让类去实现接口,并覆盖接口中的方法,从而实现类接口中定义的功能。
接口和实现类之间的关系严格上称之为“实现关系”,使用implements来表示,我们也称之为特殊的继承关系,
接口时实现类的父类,实现类就是接口的子类
面向接口编程:
接口 变量 = 创建实现类对象;//体现了多态思想。
接口和实现类的多态关系才是我们见的最多的。
类实现接口的语法:一个类可以实现多个接口,从而弥补类单继承的缺陷。
修饰符 class 实现类类名 extends 父类 implements 接口1,接口2{}
注意:
接口中的方法是公共抽象的,所以实现类必须覆盖接口中的方法,并且方法必须使用public修饰。
interface IWalk{ void walk(); } class Cat implements IWalk{ @Override public void walk() { System.out.println("walk"); } } public class test { public static void main(String[] args){ Cat c = new Cat();//鄙视 IWalk w = new Cat();//面向接口编程,存在多态 w.walk();//体现多态特征,执行Cat的walk方法 } }
面向接口编程
接口和抽象类的区别:
相同点:
1.都位于继承的顶端,用于被其他类实现或继承
2.都不能实例化
3.都可以定义抽象方法,其子类都必须覆写这些抽象方法
不同:
1.接口没有构造方法,抽象类有构造方法
2.抽象类可包含普通方法和抽象方法,接口只能包含抽象方法(java8之前)
3.一个类只能继承一个直接父类(可能是抽象类),接口是多继承的,且实现类可以实现多个接口
4.变量:接口里默认是public static final,抽象类是默认包权限
5.方法:接口里默认是public abstract,抽象类默认是包访问权限
6.内部类:接口里默认是public static,抽象类默认是包访问权限
如果抽象类和接口可以完成相同的功能,尽量选择接口,面向接口编程。
面向接口编程:多态的好处
把实现类对象赋给接口类,屏蔽了不同实现类之间的差异,从而可以做到通用编程。
interface IUSB{ void swap(); } class Mouse implements IUSB{ @Override public void swap() { System.out.println("鼠标在移动"); } } class Print implements IUSB{ @Override public void swap(){System.out.println("打印机在打印");} } class MotherBoard { public void pluginIn(IUSB iusb){iusb.swap();} } public class test { public static void main(String[] args){ MotherBoard motherBoard = new MotherBoard(); motherBoard.pluginIn(new Mouse()); motherBoard.pluginIn(new Print()); } }
内部类
//out称为外部类或宿主类,in称为内部类或嵌套类 public class out{ public class in{ } }
1.增强封装,把内部类隐藏在外部类内部,不允许其他类访问该类
2.内部类能提高代码的可读性和可维护性,把小型类嵌入到外部类中结构上代码更靠近
3.内部类可以直接访问外部类的成员
内部类根据使用不同的修饰符或者定位的位置不同,分成四种
四种内部类:
1.实例内部类:内部类没有使用static修饰
2.静态内部类:内部类使用了static修饰
3.局部内部类:在方法中定义的内部类
4.匿名内部类:适合于仅使用一次的类,属于局部内部类的特殊内部类
外部类的访问修饰符:要么使用public,要么缺省
内部类看做是外部类的一个成员,那么内部类可以使用public/缺省/protected/private修饰,还可以是static修饰
实例内部类:
没有使用static修饰的内部类,说明内部类属于外部类的实例,不属于外部类本身
1.创建实例内部类前必须存在外部类对象,通过外部类对象创建内部类对象(当存在内部类对象时,一定存在外部类对象)
2.实例内部类的实例自动自动持有外部类实例的引用,内部类可以直接访问外部类成员
3.外部类中不能直接访问内部类成员,必须通过内部类实例去访问
4.实例内部类中不能定义静态成员,只能定义实例成员
5.如果实例内部类和外部类存在同名的字段或方法abc,那么在内部类中
this,abc表示访问内部类成员
外部类.this.abc表示访问外部类成员
class Outter{ private String name = "张三"; public void getAge(){ System.out.println((this.new Inner()).age); } class Inner{ int age = 17; String name = "Inner name"; public void getName(){ String name = "local name"; System.out.println(name); System.out.println(this.name); System.out.println(Outter.this.name); } } } public class test { public static void main(String[] args){ //创建Inner类实例 Outter outter = new Outter(); Outter.Inner inner = outter.new Inner(); inner.getName(); } }
静态内部类:
使用static修饰的内部类
1.静态内部类的实例不会自动持有外部类的特定实例的引用,在创建内部类实例时,不必创建外部类的实例
2.静态内部类可以直接访问外部类的静态成员,必须通过外部类的实例去访问
3.在静态内部类中可以定义静态成员和实例成员
4.测试类可以通过完整的类名直接访问静态内部类的静态成员
class Outter{ private String name = "张三"; static class Inner{ static int age = 17; String name = "Inner name"; public void getName(){ String name = "local name"; System.out.println(name); System.out.println(this.name); } } } public class test { public static void main(String[] args){ //创建Inner类实例 Outter.Inner inner = new Outter.Inner(); inner.getName(); System.out.println(Outter.Inner.age); } }
局部内部类(打死都不用):
在方法中定义的内部类,其可见范围是当前方法和局部变量是同一个级别
1.不能使用public,private,protected,static修饰符
2.局部内部类只能在当前方法中使用
3.局部内部类和实例内部类一样,不能包含静态成员
4.局部内部类和实例内部类,可以访问外部类的所有成员
5.局部内部类访问的局部变量必须使用final修饰(java8中时自动隐式加上final,但是依然是常量,不能是变量)
原因:如果当前方法不是main方法,那么当前方法调用完毕之后,当前方法的栈帧被销毁,方法内部的局部变量的空间全部被销毁。
匿名内部类:
匿名内部类是一个没有名称的局部内部类,适合只使用一次的类。
在开发中经常有这样的类:只需要定义一次,使用一次就可以丢弃了。
1.匿名内部类本身没有构造器,但会调用父类构造器
2.匿名内部类尽管没有构造器,但是可以在匿名类中提供一段实例初始化代码块,JVM在调用父类构造器后会执行该段代码
3.内部类除可以继承类之外,还可以实现接口
格式:
new 父类构造器(实参列表)或接口(){
//匿名内部类的类体部分
}
注意:匿名内部类必须继承一个父类或者实现一个接口,但最多只能继承一个父类或实现一个接口。
interface IUSB{ void swap(); } class MotherBoard { public void pluginIn(IUSB iusb){ iusb.swap(); } } public class test { public static void main(String[] args){ MotherBoard motherBoard = new MotherBoard(); motherBoard.pluginIn(new IUSB(){ public void swap(){ System.out.println("funny"); } }); } }
枚举类
枚举的使用:
1.枚举中都是全局公共静态常量,可以直接使用枚举类名调用。
如Enum Weekday
Weekday d = Weekday.SUNDAY;
2.因为java.lang.Enum类是索引枚举类的父类,所以所有的枚举对象可以调用Enum勒种的方法
String name = 枚举对象.name();//返回枚举对象的常量名称
int ordinal = 枚举对象.ordinal();//返回枚举对象的序号,从0开始
String str = 枚举对象.toString();返回枚举对象的常量名称
3.编译器生成的枚举类的静态方法
枚举类型[] values();
Weekday[] ws = Weekday.values();//返回当前枚举的所有常量
枚举类型 valueOf(String name)
Weekday day = Weekday.valueOf(“MONDAY”);//返回指定名称字符串转换为当前枚举的常量
4.从java5开始出现枚举,switch开始支持枚举类型
switch只支持int类型,支持枚举是因为底层使用枚举对象的ordinal,而ordinal的类型仍然是int类型
String类
字符序列:把多个字符按照一定的顺序排列起来
字符串:把多个字符连接在一起
字符串分类:
不可变的字符串:String:当前对象创建完毕之后,该对象的内容(字符序列)是不能改变的,一旦内容改变,就是一个新的对象
可变的字符串:StringBuilder/StringBuffer:当前对象创建完毕之后,该对象的内容可以发生改变,当内容发生改变的时候,对象保持不变。
String,StringBuilder,StringBuffer都是接口CharSequence的实现类
字符串的本质是什么?
Char表示一个字符,数组表示同一种类型的多个数据。
String str = "ABCDEF";
Char[] s = {'A','B','C','D','E','F'};
以上本质上是相同的。
String:当前对象创建完毕之后,该对象的内容(字符序列)是不能改变的,一旦内容改变,就是一个新的对象
如下代码:
String str = "hello";
str = str + " world";
虽然变量名没有改变,但是引用地址已经修改。
String对象创建
1):直接赋一个字面量:String str1 = "ABC";
2):通过构造器创建: String str2 = new String("ABC");
Q:两种方式有什么区别,分别在内存中如何分布?
A:
1):String str1 = "ABC";
最多创建一个对象,最少不创建对象
常量池已经存在"ABC",那么str1直接引用,不创建对象
否则,先在常量池中创建"ABC"内存空间,再引用
2):String str2 = new String("ABC");
最多创建2个对象,最少创建一个对象
new关键字肯定会在堆中创建内存区域,所以至少会创建一个String对象。
如果常量池中存在"ABC"则不再创建,直接引用。否则需要创建后再引用。
常量池:专门存储常量的地方,都指的是方法区中
编译常量池:把字节码加载进JVM的时候,存储的是字节码的相关信息。
运行常量池:存储常量数据
String对象的空值:
1):表示引用为空(null): String str = null;没有初始化,没有分配内存空间
2):内容为空字符串: String str2 = "";已经初始化,分配内存空间,不过没有数据。
判断字符串非空:
1):引用不能为空null
2):字符内容不能为空字符串
字符串的比较操作:
1):使用"=="号,只能比较引用的内存地址是否相同
2):使用equals方法:在object类中和"=="相同,建议子类覆盖equals方法。
String类覆盖equals方法,比较的是字符串的内容。
Q:下面代码创建字符串的方式中有什么不同?
public class test{ public static String get(){ return "AB"; } public static void main(String[] args){ String str1 = "ABCD"; String str2 = "A" + "B" +"C" +"D"; String str3 = "AB" + "CD"; String str4 = new String("ABCD"); String temp = "AB"; String str5 = temp + "CD"; String str6 = get() + "CD"; } }
反编译后的代码
public class test { public static String get() { return "AB"; } public static void main(String[] args) { String str1 = "ABCD"; String str2 = "ABCD"; String str3 = "ABCD"; String str4 = new String("ABCD"); String temp = "AB"; String str5 = temp + "CD"; String str6 = get() + "CD"; } }
String对象比较:
1.单独使用==引号创建的字符串都是直接量,编译期就已经确定存储到常量池中。
2.使用new String创建的对象会存储在堆内存中,是运行期才创建的
3.使用只包含直接量的字符串连接符如”aa”+”bb”创建的也是直接量,编译期就能确定,已经确定存储在常量池中
4.使用包含String直接量(无final修饰)的字符串表达式,如”aa”+s1,创建的对象是运行期才创建的,存储在堆中。
通过变量/调用方法去连接字符串,都只能在运行时期才能确定变量的值和方法的返回值,不存在编译优化操作,性能不高。
String常用方法
1):String的创建和转换
byte[] getBytes() 把字符串转换为byte数组
char[] toCharArray() 把字符串转换为char数组
String (byte[] bytes) 把byte数组转换为字符串
String (char[] value) 把char数组转换为字符串
2):获取字符串信息
int length() 返回此字符串长度
char charAt(int index) 返回指定索引处的char值
int indexOf(String str) 返回指定子字符串中第一次出现处索引
int lastIndexOf(String str) 返回指定字符串在此字符串中最右边出现的索引
3):字符串的比较判断
boolean equals(Object anObject)将此字符串与指定的对象比较
boolean equalsIgnoreCase(String anotherString) 将此String与另一个String比较,忽略大小写
boolean contentEquals(CharSequence cs) 将此字符串与指定的CharSequence比较。
4):字符串大小写转换
String toUpperCase()
String toLowerCase()
Q:String,StringBuilder和StringBuffer的区别?
public class test{ public static void main(String[] args){ testString(); testStringBuilder(); testStringBuffer(); } private static void testString(){ long begin = System.currentTimeMillis(); String str = ""; for(int i = 0; i < 30000; i++){ str=str+i; } long end = System.currentTimeMillis(); System.out.println(end-begin); } private static void testStringBuilder(){ long begin = System.currentTimeMillis(); StringBuilder str = new StringBuilder(); for(int i = 0; i < 30000; i++){ str.append(i); } long end = System.currentTimeMillis(); System.out.println(end-begin); } private static void testStringBuffer(){ long begin = System.currentTimeMillis(); StringBuffer str = new StringBuffer(); for(int i = 0; i < 30000; i++){ str.append(i); } long end = System.currentTimeMillis(); System.out.println(end-begin); } }
String做字符串拼接更耗时,原因是String内容每次修改时都会创建新的对象,StringBuilder性能最高。
StringBuilder和StringBuffer都表示可变的字符串,功能和方法都是相同的。
唯一的区别:
StringBuffer:StringBuffer中的方法都使用了synchronized修饰符,表示同步的,在多线程并发的时候可以保证线程安全。
StringBuilder:StringBuilder中的方法没有使用synchronized修饰符,不安全,但性能较高。