封装(encapsulation)是面向对象的三大特征之一,它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改,而只暴露该暴露的。
比如Person类的成员变量age,只能随着岁月的流逝才会增加,因此age变量是不能随意修改的,对一个类或者对象实现良好的封装:
为了实现良好的封装,需要从两个方面考虑:
Java提供了3个访问控制符,private、protected和public,分别代表了三个访问级别,就控制级别而言,当不加任何访问控制符时被认为是default,控制级别由小到大如图所示
public class Person
{
// 使用private修饰成员变量,将这些成员变量隐藏起来
private String name;
private int age;
// 提供方法来操作name成员变量
public void setName(String name)
{
// 执行合理性校验,要求用户名必须在2~6位之间
if (name.length() > 6 || name.length() < 2)
{
System.out.println("您设置的人名不符合要求");
return;
}
else
{
this.name = name;
}
}
public String getName()
{
return this.name;
}
// 提供方法来操作age成员变量
public void setAge(int age)
{
// 执行合理性校验,要求用户年龄必须在0~100之间
if (age > 100 || age < 0)
{
System.out.println("您设置的年龄不合法");
return;
}
else
{
this.age = age;
}
}
public int getAge()
{
return this.age;
}
}
// 定义了name和age两个成员变量,只有在Person类内才可以操作和访问
// 在Person类之外只能通过各自对应的setter和getter方法来操作和访问他们
Java类里实例变量的setter和getter方法有非常重要的意义,如果java类中每个实例变量都被使用private修饰,并为每个实例变量都提供了public的setter和getter方法,那么这个类就是一个符合JavaBean规范的类
public class PersonTest
{
public static void main(String[] args)
{
var p = new Person();
// 因为age成员变量已被隐藏,所以下面语句将出现编译错误。
// p.age = 1000;
// 下面语句编译不会出现错误,但运行时将提示"您设置的年龄不合法"
// 程序不会修改p的age成员变量
p.setAge(1000);
// 访问p的age成员变量也必须通过其对应的getter方法
// 因为上面从未成功设置p的age成员变量,故此处输出0
System.out.println("未能设置age成员变量时:"
+ p.getAge());
// 成功修改p的age成员变量
p.setAge(30);
// 因为上面成功设置了p的age成员变量,故此处输出30
System.out.println("成功设置age成员变量后:"
+ p.getAge());
// 不能直接操作p的name成员变量,只能通过其对应的setter方法
// 因为"李刚"字符串长度满足2~6,所以可以成功设置
p.setName("李刚");
System.out.println("成功设置name成员变量后:"
+ p.getName());
}
}
Java允许将一组功能相关的类放在同一个package下,从而组成逻辑上的类库单元,如果希望把一个类放在指定的包结构下,需要在源文件的第一行非注释行放置代码:package packageName;
一旦在Java源文件中使用了这个package语句,这意味着源文件里定义的所有类都属于这个package,位于包中的的每个类的完整类名都应该是包名和类名的组合,那么如果要是使用该包下的类,那么必须使用包名加类名的组合
package leadscloud;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
使用命令javac -d .Hello.java
文件,-d选项用于设置编译生成class文件的保存位置,这里指定将生成的class文件放在当前路径下,编译完成后,发下当前路径下并没有Hello.class文件,而是在当前路径下生成了一个名为leadscloud的文件夹,该文件夹下有一个Hello.class文件
如果编译java文件的时候没有使用-d选项,将会在当前路径下直接生成Hello.class文件,而不会生成相应的文件结构,因此当我们编译java文件的时候不建议省略-d选项,即便我们想把生成的class文件放在当前路径下也应该使用-d.选项
执行文件的时候,在leadscloud文件夹所在路径下,使用命令 java leadscloud.Hello,是可以得到正确的输出的
D:\BaiduNetdiskDownload\CrazyJava\codes\05\5.4>java leadscloud.Hello
Hello World!
如果进入到生成的leadscloud路径下,直接使用java Hello命令反而会报异常
D:\BaiduNetdiskDownload\CrazyJava\codes\05\5.4\leadscloud>java Hello
错误: 找不到或无法加载主类 Hello
原因: java.lang.NoClassDefFoundError: leadscloud/Hello (wrong name: Hello)
如果仅仅是将生成的class放在某一个路径下,然后将这个目录名改成了包名,就认为这个目录就是java的包,是非常大的错误,有了目录结构不等于有了包名,为Java类添加包必须在源文件中通过package语句指定,单靠目录名是不能指定的,java的包机制需要条件
package leadscloud.sub;
public class Apple
{
}
package leadscloud;
public class HelloTest
{
public static void main(String[] args)
{
var h = new Hello();
// 使用类全名的写法
var a = new leadscloud.sub.Apple();
}
}
总是使用类的全名,写起来就非常麻烦,因此java提供了import关键字,import可以像某个Java文件中导入指定包层次下某个类或者全部类,import语句应该出现在package之后,类定义之前
一个Java源文件只能包含一个package语句,但可以包含多个import语句用于导入不同层面的类
import package.subpackage...ClassName;
import leadscloud.sub.Apple;
import package.subpackage...*; //导入包内全部类,*只能代表类不能代表包
使用了import语句后,便大大减少了实际代码中的前缀
package leadscloud;
import leadscloud.sub.Apple;
public class HelloTest
{
public static void main(String[] args)
{
var h = new Hello();
// 使用类全名的写法
var a = new leadscloud.sub.Apple();
// 如果使用import语句来导入Apple类后,就可以不再使用类全名
var aa = new Apple();
}
}
import java.sql.*;import java.util.*;
,然而当用到Date类的时候便会报错,因为java.sql.Date和java.util.Date都能够匹配,系统就糊涂了,这种情况下就要使用类的全名来解决 // 为了让引用更明确,即使使用了import语句,也还是需要使用类全名 java.sql.Date d = new
java.sql.Date();
import static package.subpackage...ClassName.fieldName|methodName;
import static package.subpackage...ClassName.*; //*只能代表静态成员变量或者方法名
import static java.lang.System.*;
import static java.lang.Math.*;
public class StaticImportTest {
public static void main(String[] args)
{
// out是java.lang.System类的静态成员变量,代表标准输出
// PI是java.lang.Math类的静态成员变量,表示π常量
out.println(PI);
// 直接调用Math类的sqrt静态方法
out.println(sqrt(256));
} }
package语句
import | import static
public classDefinition | interfaceDefinition | enumDefinition
classDefinition | interfaceDefinition | enumDefinition
构造器是个特殊的方法,这个特殊方法用于创建实例时执行初始化,构造器是创建对象的重要途径,java类必须包含一个或者多个构造器方法。
构造器最大的作用就是在创建对象时执行初始化,当创建一个对象时,系统为这个对象的实例变量进行默认初始化,这种默认的初始化把所有的基本类型的实例变量设为0(数值型实例变量)或false(布尔型实例变量),把所有的引用类型的实例变量设为null
如果想改变这种默认的初始化,让系统创建对象时就为该对象的实例变量显示指定初始值,就可以通过构造器来实现,否则系统将使用一个无参数的并且执行体为空,不做任何事情,无论如何java类至少包含一个构造器。
public class ConstructorTest
{
public String name;
public int count;
// 提供自定义的构造器,该构造器包含两个参数
public ConstructorTest(String name, int count)
{
// 构造器里的this代表它进行初始化的对象
// 下面两行代码将传入的2个参数赋给this代表对象的name和count实例变量
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
// 使用自定义的构造器来创建对象
// 系统将会对该对象执行自定义的初始化
var tc = new ConstructorTest("疯狂Java讲义", 90000);
// 输出ConstructorTest对象的name和count两个实例变量
System.out.println(tc.name);
System.out.println(tc.count);
}
}
ConstructorTest(String name, int count)
,系统就不再提供默认的构造器,因此上面的ConstructorTest
类不能再通过 new ContructorTest();
代码来创建实例,因为该类不再包含无参数的构造器public class ConstructorOverload
{
public String name;
public int count;
// 提供无参数的构造器
public ConstructorOverload(){}
// 提供带两个参数的构造器,
// 对该构造器返回的对象执行初始化
public ConstructorOverload(String name, int count)
{
this.name = name;
this.count = count;
}
public static void main(String[] args)
{
// 通过无参数构造器创建ConstructorOverload对象
var oc1 = new ConstructorOverload();
// 通过有参数构造器创建ConstructorOverload对象
var oc2 = new ConstructorOverload(
"轻量级Java EE企业应用实战", 300000);
System.out.println(oc1.name + " " + oc1.count);
System.out.println(oc2.name + " " + oc2.count);
}
}
public class Apple
{
public String name;
public String color;
public double weight;
public Apple(){}
// 两个参数的构造器
public Apple(String name, String color)
{
this.name = name;
this.color = color;
}
// 三个参数的构造器
public Apple(String name, String color, double weight)
{
// 通过this调用另一个重载的构造器的初始化代码
this(name, color);
// 下面this引用该构造器正在初始化的Java对象
this.weight = weight;
}
}