1.软件开发概述
软件生命周期
软件生命周期:软件的产生到报废的整个过程
软件生命周期内有:问题定义,可行性分析,总体描述,系统设计,编码,测试和调试,验收和运行,维护升级到报废等阶段.
- 问题定义及规划:此阶段是软件开发方与需求方共同讨论,主要确定开发目标及其可行性.
- 需求分析: 在确定软件开发可行的情况下,对软件需要实现的各功能进行详细分析.需求分析阶段是一个很重要的阶段,这一阶段做的好,将为整个软件开发项目的成功打下良好的基础.
- 软件设计: 此阶段主要根据需求分析的结果,把整个软件系统划分为大大小小的多个模块,设计出每一个模块的具体结构. 如系统框架设计,数据库设计等.软件设计一般分为总体设计和详细设计.
- 程序编码:此阶段是将软件设计的结果转换为计算机可运行的程序代码. 在程序编码中必须要制定统一,符合标准的编写规范,以保证程序的可读性,易维护性,提高程序的运行效率.
- 软件测试:在软件设计完成后要经过严密的测试,以发现软件在整个设计过程中存在的问题并加以纠正,整个测试过程分单元测试(白盒),集成测试(黑盒,功能测试,强度性能测试)以及系统测试三个阶段进行.测试的方法主要有白盒测试和黑盒测试两种. 在测试过程中需要建立详细的测试计划并严格按照测试计划进行测试,以减少测试的随意性.
6)运行维护:安装部署软件系统,修复软件中存在的bug和升级系统.在软件开发完成并投入使用后,由于多方面的原因,软件不能继续适应用户的需求.要延续软件的使用寿命,就必须对软件进行维护. 软件的维护包括纠错性维护和改进型维护两个方面.
软件设计原则:
为了提高软件的开发效率,降低软件的开发成本,一个良好的软件系统应该具有以下特点:
1)可重用性:遵循DRY原则,减少软件中的重复代码
2)可拓展性:当软件需要升级增加新的功能,挣够在先有的系统架构上方便地创建新的模块,而不需要改变软件现有的结构,也不会影响以及存在的模块.
3)可维护性:当用户需求发生变化时,只需要修改局部的模块中的少量代码即可
如何让软件系统达到上述特点,我们对模块的要求:
- 结构稳定性: 在软件设计阶段,把一个模块划分为更小的模块时,设计合理,是的系统结构健壮,以便适应用户的需求变化.
- 可拓展性:当软件必须增加新的功能时,可在现有模块的基础上创建出新的模块,该模块继承了原有模块的一些特性,并且还具有一些新的特性,从而实现软件的可重用和可拓展性.
- 可组合性: 若干模块经过组合,形成大系统,模块的可组合性提高软件的可重用和维护性,并且能简化软件开发过程.
4)高内聚性:内聚,强调一个系模块内的功能联系,每个模块只完成特定的功能,不同模块之间不会有功能的重叠,高内聚性可以提高软件的可重用性和可维护性
5)低耦合性:耦合,强调多个模块之间的关系,模块之间相互独立,修改其一个模块,不会影响其他的模块,低耦合性提高了软件的可维护性.
2.软件的开发方式:
面向过程的开发
面向对象的开发
面向过程:一中较早的编程思想,顾名思义就是站在过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先干啥后干啥.而每个功能我们都可以使用函数(类似于方法)这些步骤一步一步实现,使用的时候依次调用就可以了
面向过程的设计:
最小的程序单元是函数,每个函数负责完成某一个功能,用以接受输入数据对输入数据进行处理,然后输出结果数据.
整个软件系统有一个个的函数组成,其中作为程序入口的函数称之为主函数,主函数依次调用其它函数,普通函数之间可以相互调用,从而实现整个系统功能
面向过程的缺陷:
面向过程的设计,是采用置顶而下的设计方式,在设计阶段就需要考虑每一个模块应该分解成哪些子模块,每一个子模块又细分为更小的子模块,如此类推,直到将模块细化为一个个函数.
存在的问题:
设计不够直观,与人类的习惯思维不一致.
系统软件适应性差,可拓展性差,维护性低.
面向过程的最大问题在于随着系统的膨胀,面向过程将无法应对,最终导致系统的崩溃.未解决这一种软件危机,我们提出面向对象思想
3.软件设计之面向对象
面向对象:一种基于面向过程的新的编程思想,顾名思义就是站在对象的角度思考问题,我们把多个功能合理的放到不同对象里,强调的是具备某些功能的对象.具备某种功能的实体,我们称之为对象.
面向对象最小的程序单元是类
面向对象更加符合我们常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性.
软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚
当然上述例子仅仅只是说明了面向对象的一个特征--封装.除此以外面向对象还有两大特征,我们再具体讲解的时候再做分析.
三大特征:
(1)封装(encapsulation)
(2)继承(inheritance)
(3)多态(polymorphism)
封装是指将对象的实现细节隐藏起来,然后通过公共的方法来向外暴露该对象的功能.
继承是面向对象实现软件服用的重要手段,当子类继承父类后,子类是一种特殊的父类,能直接或间接获得父类里的成员.
多态是可以直接把子类对象赋给父类变量,但是运行时依然表现出子类的特征,这意味着同一类型的对象在运行时可能表现出不同的行为特征.
抽象:从特定的角度出发,从已经存在的一些食物中抽取我们所关注的特性、行为,从而形成一个新的事物的思维过程,是一种从复杂到简洁的思维方式。
4.对象和类的关系
类:具有相同特性(状态)和行为(功能)的对象的抽象就是类。因此对象的抽象是类,类的具体化就是对象。也可以说类的实例是对象,类实际上就是一种数据类型
类具有特性,对象的状态,用成员变量来描述,封装数据
类具有功能,对象的行为,用方法来表示
类是对象的类型/模板。创建一个对象,就是使用一个类作为构建该对象的基础。
对象是类的实例,类是对象的模板
5.类的定义
类的定义
[修饰符] class 类名
{
0~N个成员变量(字段/Field),不要叫属性(JavaBean中特殊的一种成员)
0~N个方法
}
不加static属于对象,加上static属于类
定义类的注意事项:
1)如果类使用了pubic修饰符,必须保证当前文件名和当前类名相同.
2)类名使用名称表示,类表示某一类事物,首字母大写,如果是多个单词组成使用驼峰表示法
3)在面对对象的过程中,定义类的时候,专门为描述对象提供一个类,该类不需要main方法。专门再定义一个测试类/演示类来运行程序
6.对象的创建和调用方法
1)根据类来创建对象的语法
类名 变量=new 类名(); 表示创建一种这种类型(类)的变量
2)给字段设置值
对象变量.字段名=该类型的值;
3)获取字段的值
该字段类型 变量=对象变量.字段值;
4)通过对象调用方法
对象变量.方法(实参);
class Servent {
String name;
int age;
void shopping() {
System.out.println("买菜...");
}
void cook() {
System.out.println("做饭...");
}
void wash() {
System.out.println("洗碗...");
}
}
public class Demo {
public static void main(String []args) {
// 创建一个对象
Servent s1 = new Servent();
// 给对象起名和设置年龄
s1.name = "小李";
s1.age = 18;
System.out.println(s1.name + ", "+ s1.age);
s1.shopping();
s1.cook();
s1.wash();
}
}
运行结果:
小李, 18
买菜...
做饭...
洗碗...
- 对象的实例化过程内存分析
public class Demo {
public static void main(String []args) {
// 创建一个对象
Servent s1 = new Servent();
// 给对象起名和设置年龄
s1.name = "小丽";
s1.age = 18;
// 创建另一个对象
Servent s2 = new Servent();
s2.name = "Lucy";
s2.age = 20;
// 创建另一个对象
Servent s3 = new Servent();
s3.name = "Lily";
s3.age = 22;
s2 = s3;
System.out.println(s2.name); // Lily
System.out.println(s3.name); // Lily
s1 = null;
System.out.println(s1.name); // NullPointerException
}
}
7.对象实例化过程-内存分析-生命周期
(1)对象的打印操作 其实打印出来的是对象的十六进制hashcode值,如:@74a14482。
(2)对象比较操作
对于基本数据类型来说,是比较的值。
对于引用数据类型来说,是比较的是内存中的地址值。
每次new,表示在堆中开辟一块新内存空间,不同的内存空间,地址值不同。object.equal()
(3)生命周期
对象的时候:使用new关键字,就会在内存开辟空间,此对象开始存在。
对象的结束:当堆中的对象,没有被任何变量所引用,此时该对象就成了垃圾,就等着垃圾回收器(GC)来回收该垃圾,当被回收的时候,对象就被销毁,回收垃圾的目的在于释放更多的内存空间
(4)匿名对象:没有名称的对象,创建对象之后没有赋给某一个变量。匿名对象只是在堆中开辟一块新的内存空间但是没有把该空间地址赋给任何变量,因为没有名称,匿名对象仅仅只能使用一次,使用完成之后就变成了垃圾。一般的,把匿名对象作为方法的实参传递:如new Servent()
就是一个匿名对象
8.构造方法/构造器/Constructor。
作用:(1)创建对象,必须和new 一起使用(2)完成对象的初始化操作。
特点:(1)构造器的名称和当前所在类的名称相同(2)禁止定义返回类型(3)不需要使用return语句。
其实构造器是有返回值的,返回的是当前创建对象的引用地址。
默认构造器的特点:(1)符合构造器特点。(2)无参数的(3)无方法体的(4)如果类A使用了pulic 修饰,则编译器创建的构造器也用public修饰。若没有使用public修饰,那么编译器创建的构造器也没有public修饰
构造器如果我们没有提供构造器,则编译器在编译时创建一个缺省的构造器。但是,如果我们显示定义了一个构造器,则编译器不再创建默认构造器。因此可以得出一个结论:某一个类中至少存在一个构造器
9.自定义构造器和构造器重载
显示写出构造器,则编译器就不会再产生默认的构造器
方法的重载:避免了在同一个类中,相同功能的方法名字不同的问题
构造器是一种特殊的方法,也可以存在重载
// 表示人类
class Person {
String name = null;
// 显示写出构造器,则编译器就不会再产生默认的构造器
// 自定义构造器
Person() {
System.out.println("无参数");
}
Person(String a) {
System.out.println(a);
name = a;
}
}
public class PersonDemo {
public static void main(String[] args) {
// 创建对象,其实是在调用构造器
new Person();
Person p1 = new Person("haha"); // 表示调用Person类中,带有一个String类型的参数
System.out.println(p1.name);
}
}
10.static修饰符
static修饰符表示静态的,可修饰字段,方法,内部类,其修饰的成员属于类,也就是说static修饰的资源属于类级别,而不是对象级别(只属于类,不属于对象)。
static修饰符的特点:
(1)static修饰的成员(字段/方法),随着所在类的加载而加载。当JVM把字节码加载进JVM的时候,static修饰的成员已经在内存中存在了。
(2)优先于对象的存在:对象是我们手动通过new关键字创建出来。
(3)static修饰的成员被该类型的所有对象所共享。根据该类创建出来的任何对象,都可以访问static成员,其本质依然使用类名词访问和对象没有任何关系
(4)直接使用类名访问static成员:因为static修饰的成员直接属于类,不属于对象,所以可以直接使用类名访问static成员。
11.类成员和实例成员的访问
类中的成员:字段,方法,内部类
类成员:使用static修饰的成员
实例成员:没有使用static修饰的成员
类成员只能访问类成员,实例成员只能访问实例成员
类成员直接属于类,可以通过类来访问static字段和static方法
实例成员只属于对象,通过对象来访问非static字段和非static方法
注意:通过反编译可以看出来对象其实可以访问类成员,但是底层依然使用类名来访问
在static方法中只能调用static成员
非static方法中,可以访问静态成员,也可以访问实例成员
什么时候用static,什么时候不用static?
如果这个状态/行为属于整个事物(类),就直接使用static修饰,被所有对象所共享
在开发中,往往把工具方法使用static修饰。如果不使用static修饰,则这些方法属于该类的对象,我们得先创建对象在调用方法。在开发中工具类只需要一分即可,可能创建N个对象,此时我们往往把该类设计成单例,但是还是有点麻烦。因此在一般的开发中,涉及到工具方法,为了调用简单,直接使用static修饰即可
12.定义变量的语法
数据类型 变量名=值;
变量根据在类中定义位置的不同,分成两大类;
成员变量:全局变量/字段(Field),不要称之为属性(错误).直接定义在类中,方法外面.有初始值
1):类成员变量---使用static修饰的字段.
2):实例成员变量---没有使用static修饰的字段.
局部变量:变量除了成员变量,其他都是局部变量.没有初始值
1);方法内部的变量.
2)方法的形参.
3):代码块中的变量,一对{}.
变量的初始值
变量的初始值:初始化才会在内存中开辟空间.开辟空间的目的在于存储数据
成员变量:默认是有初始值的;
(1)整数类型(byte、short、int、long)的基本类型变量的默认值为0。
(2)单精度浮点型(float)的基本类型变量的默认值为0.0f。
(3)双精度浮点型(double)的基本类型变量的默认值为0.0d。
(4)字符型(char)的基本类型变量的默认为 “/u0000”。
(5)布尔性的基本类型变量的默认值为 false。
(6)引用类型的变量是默认值为 null。
(7)数组引用类型的变量的默认值为 null。当数组实例化后,如果没有没有显式地为每个元素赋值,Java 就会把该数组的所有元素初始化为其相应类型的默认值。
局部变量:没有初始值,所以必须先初始化才能使用.
变量的作用域
变量根据定义的位置不同,各自的作用域也是不同的,看变量所在的那对{}.
成员变量:在整个类中都有效.
局部变量:在开始定义的位置开始,到紧跟着结束的花括号为止.
成员变量,可以先使用后定义.局部变量必须先定义而后才能使用
13.变量的生命周期
变量的作用域指的是变量的存在范围,只有在这个范围内,程序代码才能访问它。当一个变量被定义时,它的作用域就确定了
变量的作用域决定了变量的生命周期,作用域不同,生命周期就不一样
变量的生命周期指的是一个变量被创建并分配内存空间开始,到该变量被销毁
存在位置 | 生命周期开始 | 生命周期结束 | 在内存中的位置 | |
---|---|---|---|---|
类变量 | 字段,使用static修饰 | 当所在字节码被加载进JVM | 当JVM停止 | 方法区 |
实例变量 | 字段,没有使用static修饰 | 当创建所在类的对象的时候 | 当该对象被GC回收 | 堆 |
局部变量 | 方法形参,代码块中,方法内 | 当代码执行到初始化变量的时候 | 所在的方法/代码块结束 | 当前方法的栈帧中 |
局部变量定义后,必须显示初始化后才能使用,因为系统不会为局部变量执行初始化操作。这就意味着,定义局部变量后,系统并未为这个变量分配内存空间。直到程序为这个变量赋值时,系统才会为局部变量分配内存,并将初始化值保存到该内存中。
局部变量不属于任何类或实例,因此他总是保存在其所在方法的栈内存中。
基本数据局部变量:直接把这个变量的值保存到该变量所对应的内存中
引用数据局部变量:这个变量内存中存的是地址,通过该地址引用到该变量的实际引用堆里的对象。
栈内存中的变量无需系统垃圾回收,其往往随方法或代码块的运行结束而结束。
什么时候使用成员变量和局部变量:
(1)考虑变量的生存时间,这会影响内存开始。
(2)扩大变量作用域,不利于提高程序的高内聚。
开发中应该尽量缩小变量的作用范围,如此在内存中停留时间越短,性能也就越高。
不要动不动就使用static修饰,一般,定义工具方法的时候,static方法需要访问的变量,该变量属于类,此时才使用static来修饰
也不要动不动就使用成员变量,因为存在着线程安全问题,能使用局部变量就使用局部变量
14.packag关键字
Java中特殊的文件夹称之为包(package)
关键字:package,专门用来给当前Java文件设置包名
语法格式: package 包名.子包名.子子包:
必须把该语句作为java文件中,并且是第一行代码(所有代码之前).
注意:编译命令:javac -d . PackageDemo.java 运行命令:java abc.xyz.PackageDemo
1):package规范:
(1)包名必须遵循标识符规范/全部小写;
(2)企业开发中,包名使用公司域名倒写;例如com._zntq;
(3)在安卓中,如果package中使用了下划线__
,则不能部署在模拟器上;此时也可以使用一个字母来代替下划线;比如:package com.mzntq;
(4)语法格式:package 域名倒写.模块名.组件名.
(5)自定义的包名,不能以java开头,因为java的安全机制会检查这个引起报错
2):类的名称
类的简单名称:定义类的名称 PackageDemo;
类的全限定名称:包名.类名 com.zntq.PackageDemo;
3):在开发中,都是先有package,再定义类;
4):Java(JDK)中的包名
包名 | 描述 |
---|---|
java.lang | 语言核心类,系统自动导入,只要用Java,都会用到这个包 |
java.util | Java工具类、集合框架、时间、日历等都是用这个包 |
java.net | 网络编程接口和类,用到网络相关的应用都要用到这个包 |
java.io | 流的接口和类,用到读写文件或者图片等要用到这个包 |
java.text | Java格式化相关类,以及之后用到的国际化要用到这个包 |
java.sql | jdbc相关接口和类,操作Java连接数据库要用到这个类 |
java.awt | 抽系窗口工具集相关接口和类,做一个类似QQ一样的软件,界面得使用这个包 |
java.swing | 图形用户界面相关接口和类(可跨平台) |
15.import关键字
当A类和B类不在同一个包中,若A类需要使用到B类,此时就得让A类中去引入B类。
没有使用import之前,操作不在同一个包中的类需要全限定名来操作。
public class ImportDemo {
public static void main(String[] args) {
int[] arr = new int[]{11,-2,3,-4,0,6};
// 打印数组
String ret = java.util.Arrays.toString(arr);
System.out.println(ret);
// 对数组进行排序
java.util.Arrays.sort(arr);
// 重新打印数组
ret = java.util.Arrays.toString(arr);
System.out.println(ret);
}
}
解决方案:使用import语句直接把某个包下的类导入到当前类中。
语法格式:import 需要导入类的全限定名;
此后,在本java文件中,只需要使用类的简单名称即可。
import java.util.Arrays;
public class ImportDemo {
public static void main(String[] args) {
int[] arr = new int[]{11,-2,3,-4,0,6};
// 打印数组
String ret = Arrays.toString(arr);
System.out.println(ret);
// 对数组进行排序
Arrays.sort(arr);
// 重新打印数组
ret = Arrays.toString(arr);
System.out.println(ret);
}
}
编译器会自动去java.lang包中去寻找我们用到的类,比如String、System(这些就在java.lang的包中),所以用到这些类不需要导入。由此可以得出推论:非java.lang包的类都需要导入。
import java.util.Arrays;
import java.util.Set;
import java.util.List;
public class ImportDemo {
public static void main(String[] args) {
int[] arr = new int[]{11,-2,3,-4,0,6};
// 打印数组
System.out.println(Arrays.toString(arr));
// java.util.Set s;
// java.util.List l;
Set s;
List l;
}
}
上述代码中表示引入了java.util包下的Arrays类、Set类、List类
import java.util.Arrays;
import java.util.Set;
import java.util.List;
但是如果要用到很多java.util包下的类,就需要不断引入,因此可以使用通配符来简单代替:**import java.util.* **
import 类的全限定名:只能导入某一个类。
import 包名.子包名.* :表示会引入该包下的所有的类到当前文件中使用到的类。注意:用到哪些就加载哪些,用不到的即使是通配符也不会引入
import java.util.*:此时的*表示类名。
在Eclipse工具在,即使我们使用了通配符*,在格式化代码的时候,也会转换为N条import语句。
上边的代码中有这样一段:
Set s;
List l;
因为只定义了没有使用,因此从反编译的代码中可以看到并没有引入import java.util.Set; import java.util.List;
如果使用后,这样在反编译中就会引入import java.*
注意:编译器会默认找java.lang包下的类,但是不会去找java.lang的子包下的类。比如:java.lang.reflect.Method类,当用到该类时,也得使用import java.lang.reflect.Method进行引入。
16.静态导入(static import)
import java.util.Arrays;
public class ImportDemo {
public static void main(String[] args) {
int[] arr = new int[]{11,-2,3,-4,0,6};
// 打印数组
String ret = Arrays.toString(arr);
System.out.println(ret);
// 对数组进行排序
Arrays.sort(arr);
// 重新打印数组
ret = Arrays.toString(arr);
System.out.println(ret);
}
}
在上述的代码中,每次使用Arrays类中的静态方法。即使已经使用了import语句,但每次都需要使用Arrays类名去调用静态方法,这样使用不太方便,期望能把Arrays类中的静态成员像当前类中的静态成员一样调用,如sort(arr)。这时就要使用静态导入:
语法格式:
import static 类的全限定名.该类中static成员名
import java.util.Arrays;
import static java.util.Arrays.sort;
public class StaticImportDemo {
public static void main(String[] args) {
int[] arr = new int[]{11,-2,3,-4,0,6};
// 打印数组
String ret = Arrays.toString(arr);
System.out.println(ret);
// 对数组进行排序
sort(arr);
// 重新打印数组
ret = Arrays.toString(arr);
System.out.println(ret);
}
}
为了简便同样可以直接使用通配符*
import static 类的全限定名.* 此时的*表示当前类的任意使用到的静态成员
注意:虽然这样可以使用,但是仍然不建议这样用,因为使用了通配符引入两个类中的静态成员,而这两个类中又有重名的方法,就会导致出现问题。通过反编译工具,所谓的静态导入也是一个语法糖,是一个编译器级别的新特性(在底层还是使用类方法进行调用)。在实际开发中,不使用静态导入,因为这样会分不清楚一个静态方法或者字段到底来源于哪个类。Eclipse工具,当格式化代码的时候,就自动取消了所有的静态导入,还是变成使用类名导入。因此只需要知道有这个静态导入即可
17.理解封装思想
什么是封装?封装是面向对象的三大特征之一,另外两个特征是:继承和多态
(1)封装就是将对象的状态和行为看成一个统一的整体,将二者存放在一个独立的模块中(类)
(2)“信息的隐藏”,封装能够提供安全性,尽可能隐藏对象实现功能,想外暴露方法,保证外界安全访问功能。把所有数据信息隐藏起来,尽可能隐藏多的功能,只向外部暴露便捷的方法,以供调用。
将所有的字段都私有化,使用private关键字修饰,不准外界访问;把方法使用public修饰,允许外界访问
加了private后字段就只能在本类中访问,要想访问就需要设置setter方法并把传过来的数保存到对象之中
一般来说,所有的字段都应该隐藏起来.
封装的好处:
(1) 让调用者方便,正确的调用系统功能,防止调用者随意修改系统属性,保证安全性。
(2)提高组件的重用性
(3)达到系统之间的低耦合和高内聚。内部发生变化时,只要暴露的接口不变,就不会影响到其他模块。
高内聚:把该模块的内部数据,功能细节隐藏在模块内部,不允许外部直接干预
低耦合:该模块只需给外界暴露少量功能方法
class Human {
String name;
private int age; // 如果年龄设置成了负数,那么就会导致不安全或者不合理
// private修饰后,age只能在该类中访问,其他类无法访问
// setter方法,专门用于给对象设置age数据,并把传过来的数据保存到对象中
void setAge(int a) {
if (a < 0) {
System.out.println("年龄不能为负数");
} else {
age = a; // 把参数的值,赋给age变量
}
}
}
public class HumanDemo {
public static void main(String[] args) {
// 创建一个Human对象
Human p = new Human();
p.name = "Well";
p.setAge(-17);
// System.out.println(p.name + ", " + p.age);
}
}
18.权限访问修饰符
封装其实就是让有些类看不到另外一些类里边做了什么事情。所以Java提供了访问权限修饰符来规定在一个类里面能看到什么,能暴露什么
访问权限控制
private 表示私有的,表示类访问权限,只能在本类中访问,离开本类之后就不能直接访问;
不写(缺省):表示包私有,表示包访问权限,访问者的包必须和当前定义类的包相同才能访问(子包也不行);
protected: 表示子类访问权限,同包中的类可以访问,即使是不同包,但是有继承关系,也可以访问;
public: 表示全域的,公共访问权限,如果某个字段/方法使用了public修饰,则可以在当前项目中任何地方访问。
修饰符 | 类内部 | 同一个包 | 子类 | 任何地方 |
---|---|---|---|---|
private | ✔️ | |||
无 | ✔️ | ✔️ | ||
protected | ✔️ | ✔️ | ✔️ | |
public | ✔️ | ✔️ | ✔️ | ✔️ |
一般地,字段都使用private修饰,表达隐藏,为了安全性。拥有实现细节的方法,一般使用private修饰,不希望外界(调用者)看到该方法的实现细节。
一般地,方法我们用public修饰,供外界直接调用。
一般地,我们不用缺省,即使要使用,也仅仅是暴露给同包中的其他类。
protected:一般在继承关系中,父类需要把一个方法只暴露给子类。
19.JavaBean规范
JavaBean是一种Java语言写成的可重用组件(类)。
必须遵循一定的规范:
(1)类必须使用public修饰;
(2)必须保证公共无参数构造器,即使手动提供了带参数的构造器,也必须提供无参数构造器;
(3)包含了属性的操作手段(给属性赋值,获取属性值)。
分类:
(1)复杂:UI.比如Button、Panel、Window类
(2)简单:domain,service组件,封装数据,操作数据库,逻辑运算等。(封装有字段,提供getter/setter)
成员:
(1)方法:Method;
(2)事件:event;
(3)属性:property。
属性:
(1)attribute:表示状态,Java中没有该概念,很多人把字段称之为属性(attribute),不要把成员变量叫做属性
(2)property:表示状态,但是不是字段,是属性的操作方法(getter/setter)决定的,组架中使用的大多数是property。
为了能让外界(其他类)访问到本类中的私有字段成员,我们专门提供getter以及setter方法。
getter方法:仅仅使用于获取某一个字段储存的值。
//去掉get,把首字母小写,得到name,此时name才是属性。
setter方法:仅仅用于给某一个字段设置需要储存的值。
每一个字段都得提供一对getter/setter,以后使用Edlipse工具之后getter/setter都是自动生成。
class Human {
private int age; // 年龄
private boolean sex; // 性别
private String name; // 姓名
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public boolean isSex() {
return sex;
}
public void setSex(boolean s) {
sex = s;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
}
public class Person {
public static void main(String[] args) {
Human human = new Human();
human.setAge(10);
human.setName("小明");
human.setSex(true);
System.out.println("姓名是:" + human.getName() + ", " + "年龄是:" + human.getAge()) ;
}
}
注意:
(1)如果操作字段是boolean类型的,此时不应该叫做getter方法,而是is方法,把getName变成isName.
(2)在JavaBean中有属性这个概念,只有标准情况下字段名和属性名才相同。
20.this关键字
this表示当前对象,主要存在于两个位置中:
(1)构造器中:就表示当前创建的对象
(2)方法中:哪一个对象调用this所在的方法就表示哪一个对象。
当一个对象创建之后,JVM会分配一个引用自身的引用this
// 用户信息类
class User {
private String name; // 名称
private int age; // 年龄
// 构造器重载的互调
// 默认构造器 无参数构造器
User() {
// this(""); 或者不写
// System.out.println("调用了无参数的构造器");
}
// 创建对象,初始化name 带一个参数构造器
User(String name) {
// this(); // 这句话只能写第一行 但是根据少参数调用多参数的原则 需要再修改一下
// System.out.println("调用了我, " + name);
// this.name = name;
this(name, 0);
}
// 创建对象,初始化name和age 带两个参数的构造器
User(String name, int age) {
// this.name = name;
// User(name);
/*
* 使用这种方法直接调用,编译器会报如下的错误,因为用User(name)虽然能够解决代码的重复问题,但是
* 当成了一个User方法进行了调用,注意方法和构造器是不一样的,这里没有User的方法,只有User的构造器
* 所以就会报这样的错误
* Error:(25, 8) Gradle: 错误: 找不到符号 符号:方法 User(String) 位置: 类 User
*/
// System.out.println("调用了我, " + name);
// 针对上边的问题,我们这样子调用
// this(name);
// 这个就是构造器重载的互调,表明在调用参数为String类型的构造器,
// 为了验证可以在String参数类型的构造器中加一句打印的语句
// 注意单独的使用this的时候我们叫做关键字,当他后面跟语句的时候我们称为this语句
// 当我们在这个语句上边增加一个打印输出语句的时候System.out.println("调用了我, " + name);编译器
// 会报错 Error:(36, 12) Gradle: 错误: 对this的调用必须是构造器中的第一个语句。也就说明构造器重载
// 互相调用,this必须写在构造方法第一行
// 根据少参数调用多参数上编代码再一次进行修改
this.name = name;
this.age = age;
}
// 上边的三个是构造器的重载,代码本身没问题,但是还是存在一些问题:(1)代码的功能重复,比如this.name = name
// (2)代码重复会导致代码维护性低
// 上边三个构造器的调用 当多个构造器重载时或者多个方法重载时,一般的是少参数调用多参数的。因为参数越多,该方法
// 考虑的未知因素也越多,也就是说功能更强大
public String getName() {
return name; // 返回name字段的值
}
public void setName(String name) {
this.name = name; // 把调用者传递的n参数值赋值给name
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void show() {
System.out.println(this.name + ", " + this.age);
}
public void sayHello() {
System.out.println("Hello");
this.show(); // 同类中调用实例方法,可以省略this,但是建议不要省略,会更直观些
}
}
public class ThisDemo {
public static void main(String[] args) {
// 调用无参数的构造器
User u1 = new User();
// 给u1对象设置名为Lucy
u1.setName("Lucy"); // 间接的操作私有字段
// 设置年龄
u1.setAge(18);
// u1.show();
u1.sayHello();
User u2 = new User("LiLei", 10);
u2.sayHello();
}
}
使用this的六个场景:
- 解决成员变量和参数(局部变量)之间的二义性,必须使用
- 同类中实例方法互调
- 将当前对象作为参数传递给另一个方法
- 将当前对象作为方法的返回值(链式方法编程)
- 构造器重载的互调,this([参数])必须写在构造方法的第一行
- static不能和this一起使用(当字节码被加载进JVM,static成员已经存在了,但此时对象还没有创建,而this是对象中指向自己的一个引用,因此不能一起使用,如在main方法中打印this,会报“无法从静态上下文中引用非静态变量this的错误”)
21.构造器和setter方法选用
创建对象并给对象设置初始值有两种方式:
①先通过无参构造器创建出一个对象,再通过对象调用相应的setter方法
②直接调用带有参数的构造器,创建出来的对象就有了初始值.
通过构造器和通过setter方法都可以完成相同的功能.
给对象设置数据:
(1)setter注入(属性注入)
(2)构造注入.
如何选择?
(1)如果存在带参数的构造器,方式②是比较简洁的
(2)如果在构建对象的时候需要初始化多个数据,如果使用方式②,那么构造器需要提供N个参数,参数过大不直观,此时方式①就比较简单明了了.
一般两种方式都可以选择,但针对于某些特殊的应用场景就需要考虑是否合理的问题了,如下:
(3)圆对象,如何画一个圆?必须根据半径来确定对象,就应该在构建圆对象的时候,就要确定半径值.而不是先画圆再确定半径,这样就不合理了。因此有时候需要根据数据来构建对象,此时优先选用构造器方式.
下边是一个判断点和圆位置关系的例子
// 点对象
class Point {
private double x; // 横坐标
private double y; // 纵坐标
// 构造器 setter方法
Point(double x, double y) {
this.x = x;
this.y = y;
}
// getter方法
public double getX() {
return x;
}
public double getY() {
return y;
}
}
// 圆对象
class Circle {
private double r; // 半径
// 构造器
Circle(double r) {
this.r = r;
}
// 判断点与圆的位置关系,返回有三种结果 -1在圆内,0在圆周上,1在圆外
// 参数:需要判断的点对象
int judge(Point p) {
double distance = p.getX() * p.getX() + p.getY() * p.getY();
double rr = this.r * this.r;
if (distance > rr) {
return 1;
} else if (distance < rr) {
return -1;
} else {
return 0;
}
}
}
public class CirclePointDemo {
public static void main(String[] args) {
// 创建一个点对象
Point p = new Point(3, 4);
// 创建一个圆对象
Circle c = new Circle(5);
// 判断点与圆的对象
int result = c.judge(p);
System.out.println(result);
}
}