面向对象的几大特征
-
封装
封装是一种对象功能内聚的表现形式,是的模块之间耦合度变低,更具有维护性。其主要任务是对属性、数据部分内部敏感行为实现隐藏。 -
继承
继承使子类能够继承父类,获得父类的部分属性和行为,使模块更具有复用性。但要认清继承滥用的危害性,即方法污染和方法爆炸。
方法污染:指父类具备的行为,传递给子类,但子类不具备执行此行为的能力。比如鸟会飞,鸵鸟继承鸟,但是鸵鸟飞不了,这就是方法污染。
方法爆炸:指继承树不断扩大,底层拥有的方法虽然能够执行,但是由于方法众多,经过多次继承后达到上百个方法,造成了方法爆炸。 -
多态
多态使模块在复用性基础上,更加有扩展性,在编译层面无法确定最终调用的方法,在运行期由JVM进行动态绑定,调用合适的覆写方法体来执行,即根据运行时的实际对象类型使得同一方法产生不同的运行结果,达到一个行为具有不同的表现形式。
Java面向对象编程
-
构造器
在创建对象时自动被调用的特殊方法,其主要功能是为对象的成员变量初始化,它有如下几个特征:
1、构造器的名称必须与类名完全相同。
2、构造方法没有返回类型(它返回对象的地址,并赋值给引用变量)。
3、构造方法不能被继承、重写,不能直接被调用。
4、类在定义的时候,默认提供了无参构造函数。
5、构造方法可以私有化。
6、在抽象类中有构造方法,但是在接口中没有构造方法。(因为接口是纯虚的,没有成员变量,主要关注的是里面的方法,而方法是不需要初始化的,另外由于可以实现多个接口,如果接口有构造方法,则不好决定构造器链的调用顺序)构造器的调用途径
1、通过new关键字
2、在子类构造方法中,通过supper关键字调用父类的构造方法(必须出现在第一行)。
3、通过反射方式获取并调用:
package reflection;
import java.lang.reflect.Constructor;
public class ReflectionStudy {
private ReflectionStudy(){
System.out.println("无参构造方法");
}
public ReflectionStudy(String name,int age) {
System.out.println("public有参构造方法,参数为:"+name+","+age);
}
private ReflectionStudy(String name ,int age,String sex) {
System.out.println("private 有参构造方法,参数为:"+name+","+age+","+sex);
}
public static void main(String[] args) throws Exception{
//第一步,先获取Class对象
Class cls=ReflectionStudy.class;
//通过Class.newInstance(),只能调用无参构造方法,公有,私有都可以
cls.newInstance();
//第二步。获取Constructor ,getConstructor方法需要传入参数的class对象
Constructor cto=cls.getConstructor(String.class,int.class);
//第三步,调用Constructor.newInstance方法,传入参数,进行初始化
cto.newInstance("小红",20);//调用public ReflectionStudy(String name,int age)
//调用私有化构造方法要使用getDeclaredConstructor,然后在设置setAccessible 为true
Constructor cto2=cls.getDeclaredConstructor(String.class,int.class,String.class);
cto2.setAccessible(true);
cto2.newInstance("小明",30,"男");//调用private ReflectionStudy(String name ,int age,String sex)
}
}
打印结果为:
无参构造方法
public有参构造方法,参数为:小红,20
private 有参构造方法,参数为:小明,30,男
创建类对象时,父子类加载初始化执行顺序:
1、父类静态代码块(只运行一次)
2、子类静态代码块
3、父类实例变量初始化、非静态代码块(new 一次,执行一次)
4、父类构造器
5、子类实例变量初始化、非静态代码块
6、子类构造器
eg:
package reflection;
public class Test {
public static void main(String[] args) {
new Circle();
}
}
class Draw {
public Draw(String type) {
System.out.println(type+" draw 构造器");
}
//静态代码块,只有在加载类的时候执行一次
static {
System.out.println("draw静态代码块");
}
//非静态代码块,new一次,执行一次,早于构造器执行
{
System.out.println("draw代码块");
}
}
class Shape {
public Shape(){
System.out.println("父类 构造器");
}
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类代码块");
}
private Draw draw = new Draw("父类初始化实例变量");
}
class Circle extends Shape {
public Circle() {
System.out.println("子类 构造器");
}
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类代码块");
}
private Draw draw = new Draw("子类初始化实例变量");
}
打印结果为:
父类静态代码块
子类静态代码块
父类代码块
draw静态代码块
draw代码块
父类初始化实例变量 draw 构造器
父类 构造器
子类代码块
draw代码块
子类初始化实例变量 draw 构造器
子类 构造器
要先加载父类,执行父类的静态代码块,以及静态变量初始化;
执行子类的静态代码块,静态变量初始化;
然后执行父类实例变量初始化(实例变量初始化的时候用到了draw类,因此加载draw类,)、非静态代码块,按先后顺序执行,属同级;
接着执行父类构造器;
然后执行子类实例变量初始化、非静态代码块,先后顺序执行;
子类构造器,执行完毕。
-
方法重载与重写
重载
在同一个类中,如果多个方法有相同的名字,不同的参数即称为重载(因为在编译时,可以根据规则知道调用那种目标方法,所以重载也称为静态绑定)。方法签名
在编译器眼里,方法名称+参数类型+参数个数组成一个唯一键,是JVM标识方法的唯一索引(不包括方法返回值、访问权限控制符、异常类型等),JVM会通过这个唯一键决定调用哪种重载方法。JVM匹配重载方法顺序
1、精确匹配
2、如果是基本数据类型,自动转换成更大表示范围的基本类型。
3、通过自动拆箱与装箱
4、通过子类向上转型继承路线依次匹配(null可以匹配任何类型对象)
5、通过可变参数匹配方法重写
父类定义的方法达不到子类的期望,那么子类可以重新实现方法覆盖父类的方法(因为有些子类是延迟加载的,所以最终的实现需要在运行期判断,这就是所谓的动态绑定),重写的条件如下:
1、访问权限不能变小
2、返回类型能够向上转型成为父类的返回类型
3、异常也要能向上转型成为父类的异常
4、方法名、参数类型及个数必须严格一致(为了使编译器准确地判断是否为重写行为,应加@Override注解,此时编译器会自动检查重写方法签名是否一致,避免了因写错方法名或参数导致重写失败)。
5、重写只能对非晶态、非final、非构造方法进行重写。
重写遵循一个原则,一大,两小,两同,即
子类方法访问权限要大,
异常和返回类型要小,
方法名和参数必须相同 -
接口与抽象类
接口
接口是一系列方法的声明,是一些方法特征的集合,表示一种能力,一种约定。
接口使用说明:
1、接口只能用public修饰或者缺省。
2、接口中只能定义常量,默认自动用 public static final 修饰,为全局静态常量,必须在定义时指定初始值。
3、接口中所有方法都是抽象方法,默认自动用 public abstract 修饰,为全局抽象方法。
4、接口的实现类必须实现接口的全部方法,否则就变成了抽象类。
5、接口之间可以通过extends实现继承关系,一个接口可以继承多个接口,但不能继承类。抽象类
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法(只有方法声明,没有方法实现,用abstract修饰符修饰,eg:abstract void show();),则该类必须被限定为抽象类。
抽象类使用说明
1、抽象类不能实例化,但是有构造方法。
2、抽象类中可以有具体方法,也可以没有抽象方法。
3、如果继承于一个抽象类,想要为该新类创建对象,则必须实现抽象类中所有的抽象方法,否则该新类仍为一个抽象类。接口与抽象类的区别
1、都不能实例化,但是抽象类有构造方法,接口中没有。
2、接口和抽象类在被实现或继承的时候,要求必须实现所有抽象方法,否则该类必须用abstract声明为抽象类.
3、接口中只能有抽象方法(jdk1.8以后可以有静态方法、非抽象方法)且只能用public修饰,不写默认为public abstract,而抽象类没有要求。
4、接口中只能有常量,且必须初始化,抽象类可以有变量。
5、接口之间可以多继承,而类只能单继承,但是类可以实现多个接口。 -
访问权限控制符及范围
-
this与super使用说明
相同点
1、都是关键字,起指代作用。
2、在构造方法中,必须出现在第一行,this和super只能出现一个且只能出现一次。
3、this和super都在实例化阶段调用,因此不能在静态方法和静态代码块中使用。区别
this访问本类的实例属性和方法,先查找本类,没有则找父类,单独使用时表示当前对象。
super在子类访问父类的实例属性和方法,直接查找父类,在子类重写父类方法时,可以使用super访问父类同名方法;子类继承父类时,如果不指定调用父类的某个构造方法,则默认隐含调用super()。 -
泛型
对泛型的加深理解
1、尖括号里的每个元素,都指代一种未知类型。
2、尖括号的位置,必须在类名之后或方法返回值之前。
3、泛型在定义处只具备执行Object()方法的能力。使用泛型的好处
1、类型安全:放置的是什么,取出来的自然是什么。
2、提升可读性:从编码阶段就显示地知道处理的对象类型是什么。
3、代码重用:泛型合并了同类型的处理代码,使代码重用度变高。泛型的继承和通配符
在定义泛型时,可以通过extends来限定泛型类型的上限,也可以通过super来限定其下限。
比如,
List<?super Father> list,这里super包含高于的意思,即list可以存放Father的父类对象,一般使用这种方式向集合中写元素;
再有,
List<?extends Father>list,这里extends表示继承,即list可以存放Father的子类对象,一般使用这种方式从集合中读取元素。 -
需要单独分析的String对象
通过String定义常量和变量的区别
比较两种通过String类定义字符串的写法,
public static void main(String[] args) {
String s1="abc";//定义一个常量
String s2=new String("abc");//定义的是一个变量
System.out.println(s1==s2);
}
打印结果为false,String s1="abc"定义的是一个常量,在常量池中开辟了一块空间,在其中存放字符串abc,s1直接指向常量池中的abc;而String s2=new String(“abc”)通过new关键字在堆内存中开辟了一块内存,s2作为变量指向这块堆内存,而“==”比较符对于非基本数据类型比较的是地址值,所以s1不等于s2。
我们再来比较,
public static void main(String[] args) {
String s1="abc";
String s2=new String("abc");
String s3="abc";
String s4=new String("abc");
String s5="a";
String s6=s5+"bc";
String s7="a"+"bc";
System.out.println("s1==s2:"+(s1==s2));//false
System.out.println("s1==s3:"+(s1==s3));//true
System.out.println("s2==s4:"+(s2==s4));//false
System.out.println("s1==s6:"+(s1==s6));//false
System.out.println("s1==s7:"+(s1==s7));//true
}
打印结果为
s1==s2:false
s1==s3:true
s2==s4:false
s1==s6:false
s1==s7:true
s1==s2前面已经说过了,因为一个在常量池一个在堆中,地址不可能一样;
第二行输出为true,这是因为Java虚拟机会对常量池进行优化,s1和s3指向的都为常量,且值相同,因此他们是存放在一个空间中的,即s1和s3指向同一块内存;
第三行输出false是因为,通过new定义了两个变量,即使它们的值一致,但它们处在堆内存两个不同的空间,所以地址不一样;
第四行,s6=s5+“bc”,相当于一个变量s5+常量“bc”,其结果为一个变量,所以s6相当于是一个变量,而s1直接指向常量池,所以地址不同,结果为false;
第五行,s7=“a”+“bc”,相当于常量“a”+常量“bc”,其结果为一个常量“abc”,所以s1=s7,结果为true。
根据以上的例子得出结论:
1)String常量放在常量池中,通过java虚拟机的优化,会让内容一致的对象共享内存,而通过new定义的不同变量放在堆内存中,地址不会相同。
2)String常量连接一个常量,其结果仍为常量,由常量池管理,而变量连接常量,其结果为一个变量。
String的内存值不可变
通过一个小例子分析,
String s1=new String("abc");
s1=new String("bcd");
for(int i=0;i<=100;i++){
s1=s1+i;
}
第一行,在内存中开辟了一个空间其中存放abc,假设地址为1000号,s1指向1000号内存,
第二行,在内存中又开辟了一个空间存放bcd,假设地址为2000号,此时s1指向2000号内存,可以发现,只是引用变了,而内存中的值并没有改变,此时存放abc的1000号内存,由于没有对象引用,成了内存碎片,要到下次java虚拟机回收内存时,才会被回收。
for循环中频繁的进行了字符串连接操作,由于不可变性,此时会频繁的开辟新的内存空间存放新值,会造成大量的内存消耗,应该避免。
结论:
1、尽可能使用常量,而避免使用变量new的方式,如 String s=new(“aaa”);
2、尽量避免频繁的对String对象做连接操作,因为会频繁的生成内存碎片,导致内存性能问题,如果需要的话,应使用StringBuilder对象。
StringBuilder和StringBuffer
由于频繁的对字符串进行操作会产生大量的内存碎片,导致内存性能问题,因此针对这种需求可以使用StringBuilder,由于StringBuilder是字符串变量,是可变的对象,当用它进行字符串操作时,是在一个对象上操作的,这样就不会额外创建对象导致性能问题。
举个栗子,
public static void main(String[] args) {
StringBuilder builder=new StringBuilder("a");
System.out.println(builder);//a
builder.append("bcdef");
System.out.println(builder);//abcdef
builder.substring(0,2);
System.out.println(builder);//abcdef
String buil=builder.substring(0,2);
System.out.println(buil);//ab
builder.replace(1, 3, "w");
System.out.println(builder);//awdef
}
输出结果为,
a
abcdef
abcdef
ab
awdef
分析,首先创建个StringBuilder 对象,并赋值为a;然后通过append方法进行连接操作,得到字符串abcdef;builder.substring()方法返回的是String类型,并没有把结果写到StringBuilder中,所以应该用String来接收builder.substring(0,2)的内容,从第零个元素截取到第二个元素,包头不包尾,打印ab;通过builder.replace(1, 3, “w”)替换字符,将第一个元素到第三个元素替换为w,包头不包尾,打印awdef。
StringBuffer和StringBuilder使用类似,这里就不再举栗了。需要注意的是,String Builder是线程不安全的,而StringBuffer是线程安全的,由于要保证线程的安全,所以String Buffer性能要低于StringBuilder,如果在单线程环境下,应该使用StringBuilder。