zoukankan      html  css  js  c++  java
  • Java面向对象(上)

     Java是一种面向对象设计的高级语言,支持继承、封装和多态三大基本特征,首先我们从面向对象两大概念:类和对象(也称为实例)谈起。来看看最基本的类定义语法:

    复制代码
    /*命名规则:
     *类名(首字母大写,多个单词组合时每个单词首字母大写,单词之间不加任何连接符号)
     *字段名、方法名(首字母小写,多个单词组合时第一个单词首字母小写,之后每个单词首字母大写,单词之间不加任何连接符号)
     */
    [public][final] 类名 {
        [public|protected|private 类名() {}] //构造器
        [public|protected|private][static|final] 类型 字段名 [=默认值];//字段列表
        [public|protected|private][static|final|abstract] 返回值 方法名(参数) {} //方法列表
    }
    复制代码

    从以上的语法中发现几个知识点:(1).构造器名称为类名相同且没有任何返回值(甚至都不能返回void);(2).类的修饰符要么为public,要么没有;(3).字段可以添加默认值;(4).方法的修饰符中final和abstract不能同时使用;(5).字段与方法都可以使用static进行修饰。基于这些,我先解释1,3,5,剩下的将在后续的讲解中逐渐涉及到。首先为什么构造器没有任何返回值呢?实质上,在Java中当利用new来调用构造器时是有返回值的,总是返回当前类的实例,因此无须定义返回值类型,但也不能显式使用return来返回,因为构造器的返回值是隐式的。其次,可以在声明字段的同时为之添加默认值,如未添加系统会自动添加默认值。最后,使用static修饰的字段和方法我们称为类字段和类方法,可以使用类和实例来调用,但是在类方法中不能访问任何实例字段和实例方法(即没有static修饰)。

         利用类的构造器来创建类的实例,如未提供构造器,系统将默认提供一个无参构造器,如提供构造器,系统则不会提供默认构造器(因此自定义构造器的同时建议添加一个无参构造器)。

    复制代码
    class Person {
        public String name;
        public int age;
        
        public Person() {}
        public Person(String n, int a) {
            name = n;
            age = a;
        }
        
        public void display() {
            System.out.println("Name: " + name + ", Age: " + age);
        }
    }
    
    public class TestPerson {
        public static void main(String[] args) {
            Person p = new Person();
            p.display();//Name: null, Age: 0
            p.name = "Miracle";
            p.age = 28;
            //p = new Person("Miracle", 28);
            p.display();//Name: Miracle, Age: 28
        }
    }
    复制代码

    与上篇提到过的数组一样,类也是引用类型,以上Person类定义的引用变量p存放在栈内存中(只是存放实际Person对象的地址,没有任何实际数据,类似于C的指针),而实际的Person对象存放在堆内存中,下图显示了Person对象初始化完毕后的内存分布情况。

    因此对于引用类型来说,栈内存仅仅存放引用类型变量,即实际对象的地址,而堆内存则存放实际对象的数据,通过引用变量来引用实际对象,如果将该引用变量赋值为另一个引用变量,仅仅将两个引用变量指向同一个对象而已,而不会发生复制对象数据
          提到引用指针,就不得不提到this引用了,它代表当前类正在执行的实例。主要有三种用法:(1).用于区分方法(含构造器)的形参与类字段同名时;(2).用于同一个类实例方法之间调用时;(3).用于同一个类重载构造器之间调用时。来看以下代码:
    复制代码
    class Person {
        public String name;
        public int age;
        
        public Person() {}
        //重载构造器之间调用时,需使用this
        public Person(String name) {
            this(name, 0);
        }
        //构造器中形参与字段同名,需使用this
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        //方法中形参name与字段name同名,需使用this
        public void changeName(String name) {
            this.name = name;
            //方法中还引用实例方法display,此时可使用this,也可不使用
            this.display();
            //display();
        }
        
        public void display() {
            System.out.println("Name: " + name + ", Age: " + age);
        }
    }
    
    public class TestPerson {
        public static void main(String[] args) {
            Person p = new Person("Miracle", 28);
            p.display();
            p.changeName("Miracle He");
        }
    }
    复制代码

      方法是类或对象行为特征的抽象,跟C语言的函数相似,但也有显著的不同。方法必须属于类或对象,只能在类中定义,函数则是结构化语言的组成单元。方法可以采用static进行定义,代表这个方法是属于类或对象,但在static方法中不能调用this或其他非static方法或字段。Java方法的参数传递方式只有一种:值传递,即将实参的副本传递给方法,参数本身不发生任何变化。先看看基本类型参数的传递示例:

    复制代码
    public class TestPassPrimitiveArgs {
        public static void swap(int a, int b) {
            System.out.println("交换前,a = " + a + ", b = " + b);//a = 3,b = 5
            int temp = a;
            a = b;
            b = temp;
            System.out.println("交换后,a = " + a + ", b = " + b);//a = 5,b = 3
        }
        public static void main(String[] args) {
            int a = 3;
            int b = 5;    
            swap(a, b);
            System.out.println("交换结束后,a = " + a + ", b = " + b);//a = 3,b = 5
        }
    }
    复制代码

    从运行结果来看,在swap()中交换之前是3和5,交换后变成5和3,而实参在main()中始终变成不变,因此在main()传递给swap()的实参只是a和b的副本,而不是a和b本身。我们以内存分布来说明执行状况,当在main()中传参给swap()时,实际上就是在main()方法栈区向swap()方法栈区传递一份a和b的副本,如下图:

    当执行swap()时,swap()方法栈区将a和b副本进行交换,交换完成后进入main()方法栈区,此时仅仅a和b的副本发生改变,其本身没有发生任何变化。接下来我们来看看引用类型的交换,前面我们说了只能通过值传递的方式来传参,可能对有些朋友来说稍显疑惑。

    复制代码
    class DataSwap {
        public int a;
        public int b;
    }
    public class TestPassReferenceArgs {
        public static void swap(DataSwap ds) {
            System.out.println("交换前,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 3, ds.b = 5
            int temp = ds.a;
            ds.a = ds.b;
            ds.b = temp;
            System.out.println("交换后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3
        }
        public static void main(String[] args) {
            DataSwap ds = new DataSwap();
            ds.a = 3;
            ds.b = 5;
            swap(ds);
            System.out.println("交换结束后,ds.a = " + ds.a + ", ds.b = " + ds.b);//ds.a = 5, ds.b = 3
        }
    }
    复制代码

    从运行结果来看,确实不仅在swap中交换成功,在main中仍然是交换之后的结果。让人一下觉得:从main中传递给swap似乎不是ds对象的副本了,而是ds本身,这与我们前面谈到的Java方法传参只能按值传递相违背了,下面我详细说明一下。我们都知道,此时传递的是引用类型DataSwap,而引用类型的内存方式已经谈过了,在main()方法栈区中实际存放的是ds对象的地址,而实际的数据(a,b)是存放在堆内存中。现在将ds对象引用由main传递给swap,实际上是ds对象的地址复制一份到swap方法栈区中,此时main和swap中都已拥有ds对象的地址,且都指向在堆内存中实际存放的数据。也就是说引用类型参数数据传递方式是不折不扣的值传递方式,只不过传递的仅仅是引用变量,而不是引用变量所指向的引用类型数据。当然这里对main或swap中任何一个ds对象数据的更改都会影响到另一方,同时我们还可以验证main和swap中的ds是两个不同的引用变量,试着在swap种方法最后添加: ds=null.也就是切断swap中对ds的引用,查看一下main中ds对象的a和b是否受到影响(结果是不会)。

    接下来,谈一谈可变参数的实现方式(在类型后添加三个点...),即形参可以输入任意个参数(类似于C#的params),在看实际例子前,需要说明:可变参数必须是方法的最后一个参数,且最多只有一个可变参数,相当于传入了一个对应类型的数组(只是长度可变)。

    复制代码
    public class TestVarityArgs {
        public static void readBooks(String name, String... books) {
            if(books.length == 0) {
                System.out.println(name + " has not a book to read");
            } else {
                String result = name + " is reading: ";
                for(String book : books) {
                    result += book + " ";
                }
                System.out.println(result);
            }
        }
        public static void main(String[] args) {
            readBooks("Miracle");
            readBooks("Miracle", "Java", ".Net", "J2EE");
            readBooks("Miracle", new String[] { "Java", ".Net", "J2EE" });
        }
    }
    复制代码

    谈到了可变参数,似乎跟重载函数非常相似,都是同一个方法有多种调用形式,但是它们有着显著的区别。重载函数必须满足"两同一不同":同一个的重载方法名的必须相同,但是形参列表不同(返回值、修饰符不能作为重载的标准)。请注意,尽量别对包含可变参数的方法进行重载,因为这样可能会引起歧义。

    复制代码
        public void readBooks(String name) {
            //...
        }
        public void readBooks(String name, String book) {
            //...
        }
        public boolean readBooks(String name, String[] books, int count) {
            //...
        }
    复制代码

    我不知道是否有朋友会问返回值为什么不能作为重载的标准呢?假设现在有以下两个重载方法:int f(){}、void f() {},当执行调用时:int r = f();很明显是调用前者,但是如果没有将函数结果赋值呢?直接调用f(),此时你可能就不知道该调用谁了,当然java编译器比你还糊涂,更不知道怎么办了,因此返回值不能作为重载的标准。
      前面一直提到static这个概念,接下来我以例子来说明它的应用,可以看出static和非static字段和方法的区别所在。

    复制代码
    class Person {
        public String name;
        public int age;
        public static int courses = 2;
        
        public Person() {}
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public static void AddCourse(int count) {
            //在static方法中只能访问static字段,不能访问实例字段
            //System.out.println(name + " 's course: ");
            courses += count;
        }
        
        public void display() {
            //在实例方法中可以访问static字段
            System.out.println("Name: " + name + ", Age: " + age + ", Course: " + courses);
        }
    }
    
    public class TestPerson {
        public static void main(String[] args) {
            Person p = new Person("Miracle", 28);
            p.display();
            //static方法可通过类调用,也可通过实例调用,调用效果一致,会对该类的所有实例产生影响
            Person.AddCourse(1);//p.AddCourse(1);
            p.display();
            //p1.courses现在也变成3
            Person p1 = new Person("Miracle He", 28);
            p1.display();
        }
    }
    复制代码

    那在实际开发中,怎样例区分static和非static的引用呢?简单的建议是:如果定义的变量是用来描述每个对象的固有信息(如每个人都有姓名、年龄),则应该使用实例变量,相反如果描述的类的固有信息(如只要是人就只能有两只眼睛),则应该使用类变量

          不是说面向对象有三大特征吗?封装、继承、多态。那到底是怎么回事呢?首先封装,就是将对象的属性等信息隐藏在类的内部,仅提供给外部一些满足预设条件的方法供调用。拿上面的例子来说明:每个人的年龄只能在0~150之间来进行浮动,现在的情况是我可以随意更改年龄(想多少岁就多少岁),那肯定就不对了。我们必须将这些不满足条件的操作及时的过滤掉,Java提供了访问权限控制: private->default->protected->public(权限依次扩大)来封装内部属性和提供外部接口(对字段采用private或protected等修饰符来限制,采用getter和setter来进行有效控制)。

    复制代码
    class Person {
        private String name;
        private int age;
    
        public Person() {}
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return this.name;
        }
        public void setName(String name) {
            if(name.length() < 2 || name.length() > 20) {
                System.out.println("你设置的名字不合法");
                return;
            } else {
                this.name = name;
            }
        }
        
        public int getAge() {
            return this.age;
        }
        public void setAge(int age) {
            if(age < 0 || age > 100) {
                System.out.println("你设置的年龄不合法");
                return;
            } else {
                this.age = age;
            }
        }
        
        public void display() {
            System.out.println("Name: " + name + ", Age: " + age);
        }
    }
    
    public class TestAccessControl {
        public static void main(String[] args) {
            Person p = new Person("Miracle", 28);
            p.display();
            //p.age = 150;//此时age已经是private的,无法访问
            p.setAge(150);//这里已经不合法了,无法修改
            
            p.setName("Miracle He");
            p.setAge(35);
            p.display();
        }
    }
    复制代码

    关于访问控制符,有以下建议:类的绝大部分字段(有些少数的static字段需要public修饰)和辅助方法都采用private来修饰,并提供getter和setter访问器来对其读取和修改;如果值希望同一个包(即将讲到)中其他类访问,则不添加任何修饰符;如果只希望子类也能使用父类的成员而不被外界知晓,则采用protected来修饰;如果可以在任何地方都能访问到,则采用public来修饰
         接下来,将讨论上文中谈到的包。所谓包,就是为不同特征的类隔离起来,即使这些彼此隔离的包中包含同名的类也无所谓(就像在同一个班级中有两个都叫"Miracle"的同学,老师会叫其中一个"Older Miracle",另一个叫"Little Miracle"来区分)。一般一个类中只能包含在一个包中,且该语句只能为非注释语句的第一句。

    复制代码
    /*命名规则:
     *包名(全部小写,以公司或项目组织的顺序倒写,中间以.分隔,如: miracle.java.basic)
     *类名(首字母大写,多个单词组合时每个单词首字母大写,单词之间不加任何连接符号)
     *字段名、方法名(首字母小写,多个单词组合时第一个单词首字母小写,之后每个单词首字母大写,单词之间不加任何连接符号)
     */
    package 包名;
    [public][final] 类名 {
        [public|protected|private 类名() {}] //构造器
        [public|protected|private][static|final] 类型 字段名 [=默认值];//字段列表
        [public|protected|private][static|final|abstract] 返回值 方法名(参数) {} //方法列表
    }
    复制代码

    位于包中的类,在文件系统中也必须保持与包名相同层次的目录结构。

    复制代码
    package miracle.java.basic;
    public class Person {
        private String name;
        private int age;
        
        public Person() {}
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public void display() {
            System.out.println("Name: " + name + ", Age: " + age);
        }
    }
    复制代码
    复制代码
    import miracle.java.basic.Person;
    public class TestPerson {
        public static void main(String[] args) {
            Person p = new Person("Miracle", 28);
            p.display();
        }
    }
    复制代码

    我设计的项目组织结构如下图:

    其中实心代表系统真实存在的文件夹和文件,空心代表编译产生的文件夹和文件。根据开篇中讲解的编译指令来编译和运行程序。
    cd E:\project\src
    javac -d ..\bin TestPerson.java
    java -classpath ..\bin TestPerson
    (或: java -cp ..\bin TestPerson)

    运行完毕之后,我们发现源文件和字节码文件已经彻底分离了,并且在bin文件夹中还生成了跟src文件夹中Person类指定包对应的文件夹目录(细心的朋友可能发现跟IDE的目录结构有点相似了,是的我们今天又进步了一点)。

     
    分类: Java
  • 相关阅读:
    如何使用php实现首页和子页面之间的交互
    用php实现,打开哪个页面哪个页面就有默认的样式
    后台链接前台有关显示不显示
    上传文件的最大值,post传值的最大值得修改
    linux 操作系统的安装,本地登录及远程登录,vnc连接操作详细步骤
    滑动组件
    Numpy简介
    java对象序列化
    集合类操作需要注意的地方
    TreeMap详解
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/2739209.html
Copyright © 2011-2022 走看看