专栏简介 :java语法
创作目标:从不一样的角度,用通俗易懂的方式,总结归纳java语法知识.
希望在提升自己的同时,帮助他人,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
前言
一.封装
1.封装的思想
2.封装的应用
二.this与super的区别
注意事项:
三.static总结
1)静态变量在内存中的布局
2)静态变量的含义
3)静态变量的作用
1.修饰成员变量
2.修饰方法
3.修饰代码块
四.重载与重写的区别
五.继承与组合的区别
六.多态
1.向上转型
1.直接赋值
2.方法传参
3.方法返回
2.动态绑定
3.理解多态
4.多态的优缺点:
5.向下转型
七.抽象类
八.接口
1.Compareable与Compareor区别
1)基本区别:
2)Compareable的实现:
3)Compareor的实现:
4)比较总结:Compareable与Compareor
2.Cloneable接口
3.深拷贝与浅拷贝的区别
总结
前言
初学 java 这类面向对象的语言,难免对知识理解的不够深入,为了解决这类问题,这篇文章专门对初学 java 时理解起来较为抽象的概念做了通俗易懂的描述,希望在归纳总结知识的同时也可以对各位有所帮助与启发.
一.封装
1.封装的思想
封装的思想在我们日常生活中应用广泛,其目的是1.提高程序的安全性 2.提高代码的复用效率.在日常生活中许多事物的实现细节和繁琐的操作流程不需要让使用者知道,这时封装的思想就体现了很大的作用.例如:1.银行卡的账户和金额顾客不能修改,但密码可以修改. 2.启动汽车时不需要知道发动机,火花塞各个系统之间是如何配合使用的,只需按下点火按钮即可.
1.private修饰成员变量,成员方法, 其作用是隐藏内部的实现细节且只能在本类中调用.
2.private修饰构造方法,那么该类的对象就不能实例化,其目的是通常是实现单例模式因为单例模式通常有一个私有且静态的本类对象,一般提供公有且静态的方法访问该对象.
3.private修饰类,如果修饰的不是内部类那么这种操作一般是毫无意义的且编译器会报错,外部类存在的意义就是被实例化调用,如果对外不可见就失去了其存在的意义.private修饰的内部类也要很苛刻的要求内部类和内部类的方法声明都必须是静态的,不推荐内部类的写法,但有必要知道.
2.封装的应用
class Bank{
private String account;
private String password;
private int balance;
public String getAccount() {//银行卡账户可获得但不可修改
return account;
}
public String getPassword() {//银行卡密码可获得也可修改
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getBalance() {//账户余额用户可获得但不能修改
return balance;
}
}
class Ignite{//点火
private void spark_plug(){//火花塞工作
System.out.println("火花塞工作");
}
private void separator(){//分离器工作
System.out.println("分离器工作");
}
private void igniter(){//点火器工作
System.out.println("点火器工作");
}
public void ignite(){//发动
separator();
spark_plug();
igniter();
System.out.println("汽车发动完毕");
}
}
阿里编程规范:
Java中所有的成员变量一律使用private封装,并且根据属性的实际情况对外提供getter和setter方法。
二.this与super的区别
- 属性区别:this优先访问本类的属性,如果没有则访问父类.super只访问父类.
- 方法区别:this优先访问本类的方法.如果没有则访问父类.super只访问父类.
- 构造区别:this调用本类构造方法,super调用父类构造方法,但都必须在首行.
- 其他区别:this表示当前对象的引用,super表示父类对象的引用.
注意事项:
1.当一个子类继承自父类时,那么子类的构造方法中第一行会有一个默认的隐式super()语句,优先执行父类的构造方法.
2.this与super在构造方法中不能同时出现.有以下两点原因:
- this与super调用构造方法时,都必须置于构造方法的首行,同时出现则相互矛盾.
- this()中调用的构造方法会优先默认执行父类的构造方法,如果再调用super()就会重复执行父类构造方法,编译器无法通过.
class LaoMing{
public int Money;
public int car;
public LaoMing(int Money, int car) {
this.Money = Money;
this.car = car;
}
}
class XiaoMing extends LaoMing{
public XiaoMing(int Money, int car) {
super(Money, car);//优先执行父类构造方法
}
public void property(){
System.out.println("小明的身家是"+Money+"万"+car+"辆车");
}
}
public class Demo8_5 {
public static void main(String[] args) {
XiaoMing son = new XiaoMing(100,2);
son.property();
}
}
小明可以从父亲手里继承钱和车,此时this与super的关系是;
三.static总结
1)静态变量在内存中的布局
学习static关键字之前首先我们要知道,静态变量储存在哪里?在java中静态变量存储在方法区,很多同学以为方法区是堆区的一部分其实不然,《Java虚拟机规范》中明确说明:“尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
2)静态变量的含义
了解static的内存布局之后,我们从static的含义出发初步深入了解static关键字 .静态的反义词是动态,动态时指java程序在JVM上运行时,JVM会根据对象的需要动态开辟内存,而内存回收由JVM统一管理并分配给其他新建对象.那么相反静态就是指程序在运行之前,JVM就会为static所修饰的内容分配内存.当java程序运行到JVM时,JVM就会把类及类的成员变量储存在方法区,而方法区的线程是共享的所以static关键字所修饰的内容也是全局共享的,所以如果类的某些内容可以共享时,我们可以考虑使用static关键字修饰.
3)静态变量的作用
1.修饰成员变量
当我们在 int age 之前加上static关键字修饰后,age就不再属于对象,age属性统一交给Person类去管理(age属于Person的所有对象),此时age达到了全局共享的效果,但弊端是一个对象对age作出更改其他对象都会受影响.例如:p1.age=10,p2.age=20.这段代码执行的结果是p1和p2的age都为20.
public class Person {
String name;
static int age;
public String toString() {
return "Name:" + name + ", Age:" + age;
}
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "zhangsan";
p1.age = 10;
Person p2 = new Person();
p2.name = "lisi";
p2.age = 12;
System.out.println(p1);
System.out.println(p2);
}
}
2.修饰方法
相比于修饰成员变量,static关键字修饰的方法并没有在数据的存储上有区别,因为方法本身就存放在类的定义当中,其最大的作用就是可以直接使用类名调用方法,省去了实例化对象性的繁琐过程.但其弊端是只能调用静态的成员变量和方法,因为static所修饰的方法是全局共享的,所以如果调用非静态的方法或成员变量,它会不知道使用哪一个对象的方法或属性.注意:对象也可以调用static所修饰的内容但编译器会报警告.
3.修饰代码块
static所修饰的代码块为静态代码块.
- 静态代码块在类加载JVM时运行,且只加载一次,优先于构造方法
- 静态代码块不能存在于方法体中
- 静态代码块不能访问普通变量
无论是方法体还是普通变量都依赖于实例化对象的运行,而静态代码块在类加载时已经执行所以2和3都不成立.
class Book{
public Book(String msg) {
System.out.println(msg);
}
}
public class Person {
Book book1 = new Book("book1成员变量初始化");
static Book book2;
static {
book2 = new Book("static成员book2成员变量初始化");
book4 = new Book("static成员book4成员变量初始化");
}
public Person(String msg) {
System.out.println(msg);
}
Book book3 = new Book("book3成员变量初始化");
static Book book4;
public static void funStatic() {
System.out.println("static修饰的funStatic方法");
}
public static void main(String[] args) {
Person.funStatic();
System.out.println("****************");
Person p1 = new Person("p1初始化");
}
四.重载与重写的区别
- 重载发生在同一个类中,重写发生在子类与父类之间.
- 重写方法的权限必须大于等于父类的,重载不做要求.
- 重写的返回值类型与父类必须一致,重载不做要求.
- 重写的方法名和参数名必须一致,重载参数名不一致.
- 静态方法不可以被重写,但可以被重载.
- 重写体现运行时的多态性,重载体现编译时的多态性.
注意事项:
- 声明为final的方法不能被重写,也不能被继承.
- 声明为static的方法不能被重写,但可以被继承.
- 构造方法不能被重写,修改构造方法相当于修改父类.
了解上述区别之后我们会有疑问为什么子类的重写方法的权限要大于父类?我们可以从相反的角度来考虑.假如子类对象的权限小于父类.A a = new B();这是一个典型的向上转型的案例,a的类型为A所以a能使用什么方法取决于A类,A告诉a:你可以使用m方法在任何地方.但a的执行取决于B类.B告诉a:你可以使用m方法但仅限于本类.很明显是矛盾的.
class A{
public void m(){}
}
class B extends A{
private void m(){}
}
五.继承与组合的区别
语义区别:组合有has_a语义,例如链表有节点.继承有is_a语义,例如猫是动物.
继承与组合本质都是为了提高代码的复用效率,不同的是继承父类的实现细节可见,会破坏代码的封装性.而组合被包括的对象内部细节对外不可见,封装性好.继承与封装最本质的区别是由各自的优缺点来决定到底使用哪个.
继承的优点:
- 支持扩展,通过继承实现,但会使代码更复杂.
- 对复用代码的可修改性强,但容易牵一发而动全身.
继承的缺点:
- 父类实现细节暴露给子类破坏封装性.
- 父类修改时子类也必须修改,增加代码维护难度.
- 子类太过于依赖父类,代码耦合度高.
- 编译期就决定了父类所以不支持动态扩展.
组合的优点:
- 类内部的实现细节对外不可见,封装性好.
- 高内聚低耦合,相互独立
- 支持动态扩展,运行时可根据不同的对象选择组合对象.
组合的缺点:
- 创建对象需实现考虑好所有的组合对象,会使系统有很多对象.
如何抉择二者的使用:
- 如果没有明显的is_a关系推荐使用组合.
- 不要为了多态而使用继承,组合之间通过接口也可以达到同样的效果,而且比继承有更高的可拓展性.
六.多态
1.向上转型
通俗易懂的来讲,多态就是不同对象对同一事物或时间做出的不同反应.例如:父类是熊,子类是熊猫和灰熊.同样是熊,但熊猫饿了吃竹子,狗熊饿了吃肉.如果我们用父类熊去调用子类对象,仅仅调用"吃"这个方法就会有不同的情况.这就是多态中的向上转型.
向上转型发生时有三种不同的时机:
- 直接赋值
- 方法传参
- 方法返回
1.直接赋值
class Bear{
public void eat(){
System.out.println("吃东西");
}
}
class Grizzlize extends Bear{
@Override
public void eat() {
System.out.println("吃肉");
}
}
class Panda extends Bear{
@Override
public void eat() {
System.out.println("吃竹子");
}
}
public class Polymorphism {
public static void main(String[] args) {
Bear bear1 = new Grizzlize();
Bear bear2 = new Panda();
bear1.eat();
bear2.eat();
}
}
2.方法传参
将不同的对象作为参数传递给同一方法,该方法的参数类型为父类的引用,结果产生了不同的状态.
class Bear{
public void eat(){
System.out.println("吃东西");
}
}
class Grizzlize extends Bear{
@Override
public void eat() {
System.out.println("吃肉");
}
}
class Panda extends Bear{
@Override
public void eat() {
System.out.println("吃竹子");
}
}
public class Polymorphism {
public static void Eat(Bear bear){
bear.eat();
}
public static void main(String[] args) {
Eat(new Grizzlize());
/*Grizzlize grizzlize = new Grizzlize();等价于
Eat(grizzlize);*/
Eat(new Panda());
}
}
3.方法返回
父类引用接收返回子类对象的方法
class Bear{
public void eat(){
System.out.println("吃东西");
}
}
class Grizzlize extends Bear{
@Override
public void eat() {
System.out.println("吃肉");
}
}
class Panda extends Bear{
@Override
public void eat() {
System.out.println("吃竹子");
}
}
public class Polymorphism {
public static Panda findMyBear1(){
Panda panda = new Panda();
return panda;
}
public static Grizzlize findMyBear2(){
Grizzlize grizzlize = new Grizzlize();
return grizzlize;
}
public static void main(String[] args) {
Bear bear1 = findMyBear1();
Bear bear2 = findMyBear2();
bear1.eat();
bear2.eat();
}
}
2.动态绑定
在了解动态绑定之前我们首先要知道什么是静态绑定?静态绑定就是根据你传入的参数可以确定在编译时期就能确定你所要调用的方法,这就叫做静态绑定.而动态绑定则是在编译时期看到的方法调用并不一定是正确的,只有程序运行时才能确定.
//静态绑定
public class Polymorphism {
public static void main(String[] args) {
function();
function(1);
function(1,5);
}
public static void function(){
}
public static void function(int a){
}
public static void function(int a,int b){
}
}
//动态绑定
class Dogs extends Animal{
public Dogs(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(name+"吃骨头");
}
}
public class Polymorphism {
public static void main(String[] args) {
Animal animal = new Dogs("小狗");
animal.eat();
}
调用java的.class文件的反汇编,可以看出在编译时期调用的时Animal的eat()方法,等到运行时才能知晓到底调用哪个方法?
3.理解多态
了解向上转型,动态绑定和方法重写后我们就可以用多态的思想来实现程序了,只用关注父类的代码就可以兼容多个子类的对象.
class Bear{
public void eat(){
//无任何动作
}
}
class Grizzlize extends Bear{
@Override
public void eat() {
System.out.println("灰熊吃肉");
}
}
class Panda extends Bear{
@Override
public void eat() {
System.out.print("熊猫吃竹子");
}
}
class Raccon extends Bear{
@Override
public void eat() {
System.out.println("浣熊吃虫子");
}
}
public class Polymorphism {
public static void eats(Bear bear){
bear.eat();
}
public static void main(String[] args) {
Bear bear1 = new Grizzlize();
Bear bear2 = new Panda();
Bear bear3 = new Raccon();
eats(bear1);
eats(bear2);
eats(bear3);
}
4.多态的优缺点:
1.类调用者使用类的成本降低:
- 封装使类的调用者不必过多的关注类内部的实现细节.
- 多态使类的调用者连类的类型都不需要考虑,只需要知道这个对象具有某个方法即可.
2.降低代码的圈复杂度,避免重复使用if_else:
- 例如我们需要知道多个熊他们要吃的事物,代码如下:
3.可扩展能力强:
- 未来如果新增一种熊,只需增加这个熊的实例对象即可,不需像if_else一样改变代码的结构.
4.代码运行效率低:
- 属性无多态性,构造方法无多态性.
public static void eats1(){//if_else写法
Grizzlize grizzlize = new Grizzlize();
Panda panda = new Panda();
Raccon raccon = new Raccon();
String[] bears = {"grizzlize","panda","raccon","grizzlize"};
for (String bear1:bears) {
if (bear1.equals("grizzlize")){
grizzlize.eat();
}else if (bear1.equals("panda")){
panda.eat();
}else if (bear1.equals("raccon")){
raccon.eat();
}
}
}
public static void eats(){//多态写法
Bear[] bears = {new Grizzlize(),new Panda(),new Raccon(),new Grizzlize()};
for (Bear bear1:bears) {
bear1.eat();
}
}
5.向下转型
全面了解多态以后,我们会发现多态有一个局限性,即父类不能调用子类特有的方法,而向下转型虽然能解决这个问题但十分的不安全一般不推荐使用.如果非要使用我们可以使用instanceof这个方法来保障向下转型的安全性.例如:浣熊特有的技能为感知,如果想要使用这个方法必须将父类强转为子类.
public static void main(String[] args) {
Bear bear = new Raccon();
if (bear instanceof Raccon){
Raccon raccon = (Raccon) bear;
raccon.preception();
}
}
七.抽象类
抽象类就是将子类的共性抽取出来,最大的意义就是被继承方便后续向上转型.抽象类中可以含有多种不同的成员,但抽象类不能被实例化,只能去创建该抽象类的子类重写父类的方法,这些功能看似普通的类也能做,为什么非要使用抽象方法呢?其实抽象方法相当于多了一层检验效果,普通类在调用子类对象的方法时可能会调用到父类的,这是普通编译器不会报错,但是如果父类是抽象类就会早早报错及时发现问题.
抽象方法的特点:
- 抽象类和抽象方法必须使用abstract修饰.
- 如果一个普通类继承了抽象类就必须重写抽象类中的抽象方法.
- 如果一个抽象类A继承自抽象类B,那么抽象类A无需重写抽象类B的抽象方法,但出来混迟早要还,最后继承的普通类一定要重写所有抽象类的抽象方法.
- 抽象方法不能被private修饰,必须满足重写的规则.
- 抽象方法不能被static和final修饰.
final修饰的类不能被继承与抽象类的初衷相矛盾.static修饰抽象类会直接报错.
- 抽象类最大的作用就是为了让程序员更早的发现错误.
abstract class Bear{
public String name;//成员变量
public Bear(String name) {//构造函数
this.name = name;
}
public abstract void eat();//抽象方法
public void fangun() {//普通方法
System.out.println("熊翻滚");
}
}
八.接口
1.Compareable与Compareor区别
1)基本区别:
Compareable为自然排序位于java.lang包底下,Compareor为定制排序位于java.util包底下.顾名思义一个类只要实现了Compareable接口就认为该类拥有了排序的功能,那么该类对象的List列表和数组可以通过Collection.sort()或者Arrays.sort()排序.而Compareor则是在外部指定排序规则,作为参数传递给某些类.
2)Compareable的实现:
Compareable接口只包含一个compareTo()函数,所以如果想实现自然排序只需重写compareTo()函数即可.其比较规则如下:
- e1.compareTo(e2)>0即e1>e2.
- e1.compareTo(e2)<0即e1<e2.
- e1.compareT0(e2)=0即e1=e2.
class Student implements Comparable<Student>{
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override//比较年龄
public int compareTo(Student o) {
if (this.age>o.age){
return 1;
}else if(this.age<o.age){
return -1;
}else {
return 0;
}
}
@Override//比较姓名
public int compareTo(Student o) {//调用用String类的compareTo()方法来比较.
return this.name.compareTo(o.name);
}
}
class TestInterface{
public static void main(String[] args) {
Student student1 = new Student("zhangsan",18);
Student student2 = new Student("lisi",21);
Student student3 = new Student("wangwu",19);
Student[] students = {student1,student2,student3};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
如果我们不实现Compareable接口,那么就会报类型捕获异常(classCastException).
但自然排序有一定的缺陷,即对类的侵入性较强,如果一个月后我们要把比较年龄改为比较姓名,那么对整个程序的结构都会有较大的改动,十分的不方便这时定制排序Compareor就派上了用场.
3)Compareor的实现:
Compareor位于java.util包底下,需要重写int compare(T o1, T o2)方法.但是重写时我们会发现Compareor作为一个接口有很多的方法,那么按照接口的定义我们应当重写所有方法才对,但为什么重写int compare(T o1, T o2)方法就可以呢?
其实自JDK1.8开始接口中就可以定义三类方法:1.抽象方法 2.default修饰的普通方法 3.static所修饰的方法.除了int compare(T o1, T o2)和equals属于第一类,其余都是第二或第三类自然无需重写,又因为接口的实现类都默认继承Object类,java代码在编译为字节码文件时,equals方法已经实现了,所以编译器不会认为Compareor接口的equals方法没有实现.
内部实现:
class Student implements Comparator<Student> {
public String name;
public int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
@Override
public int compare(Student o1, Student o2) {
return o1.age-o2.age;
}
}
外部实现:
此时需要借助Arrays.sort()的重载方法,即根据指定比较器引发的顺序对指定的数组对象进行排序.
class NameCompare implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
class AgeCompare implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.age-o2.age;
}
}
public class TestInterface{
public static void main(String[] args) {
Student student1 = new Student("zhangsan", 18);
Student student2 = new Student("lisi", 21);
Student student3 = new Student("wangwu", 19);
Student[] students = {student1, student2, student3};
AgeCompare ageCompare = new AgeCompare();
NameCompare nameCompare = new NameCompare();
Arrays.sort(students,ageCompare);//按年龄比较
Arrays.sort(students,nameCompare);//按姓名比较
System.out.println(Arrays.toString(students));
}
}
4)比较总结:Compareable与Compareor
- Compareable接口比较固定像是类拥有该功能与一个具体的类绑定,Compareor接口更加的灵活,实现一个就可以为各个需要比较功能的类使用.
- Compareable接口用户可以可以实现它来完成自己的特定的比较,Compareor接口可以看成是一种算法的实现,随用随取,相当于设计模式中的算法与数据分离.
2.Cloneable接口
java中有三种实例化对象的方法分别是 new关键字 反射 以及clone().当我们传递一个对象的引用时,为了防止该对象被修改,可以克隆一份再去传递.
clone()方法存在于Object类中,调用这个方法就可以创建一个对象的拷贝,但在使用这个方法之前必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException.实现步骤如下:
3.深拷贝与浅拷贝的区别
初学者可能有一个误区,认为深拷贝还是浅拷贝取决于某个确定的函数或接口,其实不然,真正决定深浅拷贝的是我们具体的实现方式.例如Cloneable接口,如果我们按如下方式写代码就是浅拷贝.这样写会导致克隆出的副本部分变量的引用与主体指向同一个对象,一但副本改变主体也会跟着改变.
class Money {
public double m = 12.5;
}
class Person1 implements Cloneable{
public int id;
Money money = new Money();
@Override
public String toString() {
return "Person1{" +
"id=" + id +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone;
}
}
class Test{
public static void main(String[] args) throws CloneNotSupportedException {
Person1 person1 = new Person1();
Person1 person2 = (Person1) person1.clone();
person2.money.m = 199;
System.out.println(person1.money.m);
System.out.println(person2.money.m);
}
}
解决方式就是将要克隆的所有对象所涉及到的类都实现Cloneable接口.
class Money implements Cloneable{
public double m = 12.5;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person1 implements Cloneable{
public int id;
Money money = new Money();
@Override
public String toString() {
return "Person1{" +
"id=" + id +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
/*return super.clone();*/
Person1 tmp = (Person1) super.clone();
tmp.money = (Money)this.money.clone();
return tmp;
}
}
class Test{
public static void main(String[] args) throws CloneNotSupportedException {
Person1 person1 = new Person1();
Person1 person2 = (Person1) person1.clone();
person2.money.m = 199;
System.out.println(person1.money.m);
System.out.println(person2.money.m);
}
}
总结
以上就是面向对象编程的全部内容了,通过对比易混淆概念与知识延拓,相信我们对面向对象已经有了一个深入的认识,如果我的文章对你有亿点点帮助和启发,麻烦不要忘记三连哦!