包
包(package) 是组织类的一种方式。使用包的主要目的是保证类的唯一性。
例如, 你在代码中写了一个 Test 类。然后你的同事也可能写一个 Test 类。如果出现两个同名的类, 就会冲突,,导致代码不能编译通过。
咋同一个java文件下是不能使用同名的类的,
创建一个包:
注意事项:
- 创建包的时候,包名最好小写,名字一般反着来,比如百度的域名是: www.baidu.com 创包名的时候就是: com.baidu.www。
- 包名应该是一个唯一的名字,通常会用公司的域名的颠倒形式。
- 包名要和代码的存储路径相匹配(例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码)
- 如果一个类没有 package 语句, 则该类被放到一个默认包中
此时我们就创建了一个包,我们打开文件目录:
我们发现在我们创建的src目录下,创建了名为com的文件夹,我们依次点击,有以下路径:
那么我们在同一目录下,不能放同名的java文件,那么我们就可以我们创建的包里来创建一个同名的java文件:
我没发现此时并没有报错。
当我们进入这个java文件的时候,会发现会多出一行:
这也是我们以后再创建包的时候需要注意的,在创建包的时候,一定在前面加上package 包的路径。
我们上述的自动加入是因为我们在idea中创建的包中的java文件,他会自己帮我们写好,如果我们用其他来创建包的时候就要加上package 包的路径。
而且我们在使用其他包里的方法和数据的时候,一般情况下是使用 import 来导入这个包中文件:
当然我们在访问我们电脑中下载的包时,可以这样写:
或者是直接调用包:
这个就是家java中导入包的另一种形式,但是这种形式一般没有人写,一般都是在文件的开头用import来导入包。
用 * 就可以调用这个包中的所有内容:
import java.util.*;
在java的包中有很多的类,比如我们之前调用的util包:
我们可以在这个网站查看:
这些都只是一些。
这些包不会像C中一样,全部加载进来,而是我们需要谁就用谁。
但是我们知道方法是可以重载的,也就是说,不同的包中有相同的方法:
我们发现此时的Data方法在util包和在sql 包中都有定义,那么我们可以定义两个在不同包中的这个Data方法吗?
这样很好理解,因为此时我们在函数中用到了Data()方法,那么编译器该用哪一个包中Data() 方法呢?编译器不知道。
同样我们用* 来调用两个包也是一样的结果:
我没发现此时报错了,也就是说我们不能调用两个包中的同名方法。
解决方法就是使用的是哪一个包中方法给说明清楚:
静态导入包
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
这种方式导入可以跟方便的实现一些代码:
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
// 静态导入的方式写起来更方便一些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}
但是这个很少用。
常见的系统包
1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
2. java.lang.reflect:java 反射编程包;
3. java.net:进行网络编程开发包。
4. java.sql:进行数据库开发的支持包。
5. java.util:是java提供的工具程序包。(集合类等) 非常重要
6. java.io:I/O编程开发包
继承
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
看下面这个例子:
class Animal{
int name;
public void eat(){
System.out.println("Animal::eat");
}
public void sleep(){
System.out.println("Animal::sleep");
}
}
class cat{
int name;
public void jump(){
System.out.println("cat::jump");
}
public void eat(){
System.out.println("cat::eat");
}
public void sleep(){
System.out.println("cat::sleep");
}
}
class bird{
int name;
public void fly(){
System.out.println("bird::fly");
}
public void eat(){
System.out.println("bird::eat");
}
public void sleep(){
System.out.println("bird::sleep");
}
}
这个例子就是cat和bird这里两个类都有 Animal这个类中的方法和属性:
- 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
- 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
- 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义).
那么现在我们就可以让cat和bird都继承Animal类,来达到代码重用的目的。
class Animal{
int name;
public void eat(){
System.out.println(this.name + "Animal::eat");
}
public void sleep(){
System.out.println("Animal::sleep");
}
}
class cat extends Animal{
int name;
public void jump(){
System.out.println("cat::jump");
}
}
我们使用extends关键字来让cat类继承Animal类。
问: 子类继承了父类的什么?
继承了父类,除构造方法之外的所有:
public static void main(String[] args) {
cat cat = new cat();
cat.name = "mimi";
cat.eat();//mimiAnimal::eat
}
此时我的cat类中没有创建name变量和eat方法:
那么此时cat和Animal是怎么存储的呢?
cat在拥有Animal中的属性之外,还有自己的属性。
而且在java当中只有单继承,只能继承一个类:
当我们在子类中调用父类中的private修饰的属性或方法,我们不能调用,这只是不能调用,不是没有继承:
class Animal{
String name;
public void eat(){
System.out.println(this.name + "Animal::eat");
}
private void sleep(){
System.out.println("Animal::sleep");
}
}
class cat extends Animal{
String name_cat;
}
子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。
看如下例子:
class Animal{
String name;
public Animal(String name)
{
this.name = name;
}
}
我们在创建父类 Animal的时候创建一个带参数的构造方法,那么此时我们在用cat继承Animal父类的时候就会报错:
这时因为,我们在构建子类之前,要先帮助父类进行构造,而我们之前没有对Animal父类创建构造函数,那么默认就是不带参数的构造函数,那么我们不用在构建Animal的时候进行传参。但是现在我们在Animal类中构建了一个带参数的构建方法,那么此时我们就需要传参。
而我们在创建cat子类的时候没有代码来帮助父类Animal来构建,所以会报错,那么现在我们用super关键字来在子类中帮助父类构建:
class cat extends Animal{
public cat(String name)
{
super(name);
}
}
此时我们发现,不在报错。
此时的super就是引用当前对象的父类:
也就是说,在cat构造函数执行时候,先执行super到其父类的构造函数中去,待父类的构造函数初始化,执行完毕之后,再继续执行接着cat的构造函数。
super关键字使用的注意事项:
- super 关键字必须放在第一行,这个和this是一样的。
- 上述的super的使用,不是继承了父类的属性和方法,只是显示调用了父类的构造方法。
所以以后我们在继承父类的构造方法的时候,一定记着要帮助父类构建方法。
父类的属性继承过来之后,我们可以直接像使用全员变量一样去使用它:
class Animal{
String name;
}
class cat extends Animal{
public void eat(){
System.out.println(this.name);
}
}
但是如果我们在子类中,定义了一个相同名字的变量,那么子类中引用的变量,会优先实现子类中同名的变量,而不是父类中的同名变量:
class Animal{
String name = "lisi";
}
class cat extends Animal{
String name = "zhangsan";
public void eat(){
System.out.println(this.name);
}
}
public class textdome {
public static void main(String[] args) {
cat cat = new cat();
cat.eat();//zhangsan
}
}
我们发现上述结果输出了 “zhangsan” 而不是 “lisi”。
protected 关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 "封装" 的初衷。两全其美的办法就是 protected 关键字.
- 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
- 对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
protected关键字的作用主要体现在继承上。
而且如果父类中的属性是private的,那么我们在子类中不能在使用:
例子;
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
比特科技this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
// 对于父类的 protected 字段, 子类可以正确访问
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
}
// Test.java 和 Animal.java 不在同一个 包 之中了.
public class Test {
public static void main(String[] args) {
Animal animal = new Animal("小动物");
System.out.println(animal.name); // 此时编译出错, 无法访问 name
}
}
这个时候,我们又要体现封装性,又要在这个变量能在子类中使用,那么我们应该使用protected这个修饰词:
此时的name在子类中依然可以使用。
问:this关键字和super关键字有什么区别?
以下区别:
上述的super.data() 和 super.func(); 的这两个方式使用的前提是,如果需要子类帮助父类去构造方法,那么此时需要super() 帮助父类构造函数之后才能使用。
java中private protected define public 四个关键词的区别
其中的default就是没有修饰词的:
这个就是default的变量。default也叫做默认权限,包访问权限。上述的Animal类也是default的。
这个的属性和方法,类只能再同一包中访问,其他包不能进行访问。
那么以上四种修饰词,我们改如何选择呢?
我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用
public.
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对
访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内部
自己用, 还是类的调用者使用, 还是子类使用)
更复杂的继承关系
虽然一个子类只能继承一个父类,但是父类也是可以继承其他类为父类的,子类也是可以当其他类的父类的,而对于类其中的访问关系,也就是继承的子类所调用的方法和属性是最多的,如上图,鸳鸯眼可以调用山东狮子猫中华田园猫,猫,动物加上自己五大类的属性和方法。
但是,继承关系不是无限往下继承的,因为这样的话就太麻烦了,一般我们不希望出现三层以上的继承关系,如果出现了三层以上的继承关系,那么我们改考虑这些方法的重构了。
但是,java语法上是不限制继承关系层数的,我们可以使用final关键字,来进行语法上的限制继承
class Animal{
// private String name = "lisi";
protected String name = "lisi";
int age;
}
class cat extends Animal{
// String name = "zhangsan";
public void eat(){
System.out.println(this.name);
}
}
如上述代码,cat已经继承了Animal类了,但是我不想再让cat继续继承了,那么我们可以这样做:
final关键字
final int a = 10;//常量,
我们可以使用final来创建一个常量,所谓常量就是,只初始化一次,创建之后不能再被修改。
final修饰类:这个类将不能再被继承。
final修饰方法:这个方法就是密封方法。
组合
和继承类似,组合也是表达类之间的关系,之前继承关系是(is a)的关系,组合则是(has a)的关系,组合也能达到代码重用的效果。
例如一个学校里面,有老师和同学这是has a)的关系,但是不能说是老师是学校,同学是学校,这是(is a)的关系。
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
上述代码就是在一个学校里面,有老师和同学的组合,我们发现,组合中没有任何的关键字去表示这时一个组合。
上述的结构就类似于顺序表的实现,相当于是在School类中有两个数组,一个存储的是Student的属性和方法,一个存储的是Teacher的属性和方法。
多态
向上转型
还是我们之前的例子,cat类继承了Animal类,那么就有以下这种关系:
所谓向上转型就是:
父类引用,引用子类对象。
class Animal{
// private String name = "lisi";
// final int a = 10;//常量,
protected String name = "lisi";
int age;
public void eat()
{
System.out.println("Animal::eat");
}
public Animal(String name)
{
this.name = name;
System.out.println("Animal::init");
}
}
final class cat extends Animal{
// String name = "zhangsan";
public int count = 10;
public cat(String name)
{
super(name);
System.out.println("cat::init");
}
}
public class textdome {
//向上转型
public static void main(String[] args) {
Animal animal = new cat("咪咪");
}
//
public static void main1(String[] args) {
Animal animal = new Animal("Animal");
cat cat = new cat("咪咪");
}
}
上图就是上述代码中的向上转型。
但是需要注意的是:我们在向上转型之后,只能使用父类中的方法和属性,但是子类中的方法和属性,我们是不能访问的:
发生向上转型的时机
- 直接赋值;我们刚刚所实现的就是直接赋值所产生的向上转型
- 传参
- 返回值
传参:
public static void func1(Animal animal)
{
animal.eat();
}
//向上转型
public static void main(String[] args) {
cat cat = new cat("咪咪");
func1(cat);
}
此时就发生了向上转型。
打印:
返回值:
向下转型
class Animal{
public void eat()
{
System.out.println("Animal::eat");
}
public Animal(String name)
{
this.name = name;
System.out.println("Animal::init");
}
}
final class cat extends Animal{
public cat(String name)
{
super(name);
System.out.println("cat::init");
}
public void jump()
{
System.out.println("cat::jump");
}
}
如这两个类,我们创建父类引用子类对象,那么此时我们是不能访问子类中jump方法的:
如果此时我们像用这个animal这个对象来引用子类中的方法,那么我们就要进行类型的强制转换,把父类的引用强转成子类的引用:
public static void main(String[] args) {
Animal animal = new cat("mimi");
//向下转型
cat cat = (cat)animal;
cat.jump();
}
此时的jump可以被调用,上述的强转就是把animal类型强转为cat之后,再把值赋值给cat。
上述我们就实现了向下转型。
但需要注意的:
向下转型非常的不安全,很少会用到向下转型
此时我在创建一个Animal的子类:
class bird extends Animal{
int name;
public bird(String name)
{
super(name);
System.out.println("bird::init");
}
public void fly(){
System.out.println("bird::fly");
}
}
其中有一个bird独有的fly方法,那么此时我用其他对象对访问fly是不能访问的:
那么我们进行强转,向下转型不就能访问了吗?此时java小白就去强转访问了:
我没发现没有在报错了,但是当我们运行程序的时候,就报异常了:
这个叫类型转换异常,之所以发生异常是因为,我们之前成功转换是因为我们的animal引用的就是cat对象,所以我们可以强转为cat类型,但是此时我们animal引用的是还是cat对象,但是我们要把他强转为bird类型,这里就发生了类型转换异常。
我们要想不报错,就要使用instanceof关键字:
public static void main(String[] args)
{
Animal animal = new cat("mimi");
if( animal instanceof bird)
{
bird bird = (bird)animal;
bird.fly();
}
else
{
System.out.println("类型转换错误");
}
}
如此代码就可以执行了
而上述的instanceof意思如下:
A instanceof B
的意思是:判断A是不是B的一个实例,是返回true,不是返回false
在构造方法中调用重写的方法(一个坑)
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func。
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
// 执行结果
D.func() 0
也就说在构造器中调用重写的方法时候,也会发生运行时绑定。
多态和重写
例子:
class bigger{
public void better(){
System.out.println("bigger::better");
}
}
class sm extends bigger{
public void better(){
System.out.println("sm::better");
}
}
上述中sm子类中的better就是重写,
那么重写应该满足一下条件:
- 方法名称相同
- 返回值相同
- 参数列表相同
- 两个方法在不同的类中,而且两个类满足继承的关系
我们之前说过,如果发生向上整形,那么我们父类引用只能调用父类的方法,不能调用子类的。
那么此时对于上述的向上转型,我们父类引用调用的是哪一个方法中的eat呢?
public static void main(String[] args) {
bigger bigger = new sm();//父类引用子类对象
bigger.better();
}
此时打印结果为:
我们发现,在对父类中的better方法在子类中进行重写之后,我们在创建一个父类引用子类对象,发生向上转型,此时调用的better方法不再是父类中的better方法,而是子类中的better方法。
那么在上述过程当中,我们就发生了多态,这个过程也叫做运行时绑定或动态绑定。
运行时绑定发生的前提是:父类引用,引用子类对象,然后通过父类引用调用同名的覆盖方法,此时就会发生运行时绑定。
接下来我们来反编译一下java代码,来理解这里的运行时绑定:
我们在idea中打开terminal:
我们先进入到当前java文件所在的文件目录下:
然后使用javap -c 命令进行反汇编:
javap -c xxx
比如我们反汇编这个例子:
其中animal是父类,car是子类,eat在cat中重写过。
我们观察反汇编代码,发现我们在调用eat方法时候,在反汇编中调用的是Animal父类中的eat方法,但是打印结果确实调用的确实子类Cat中的eat方法
这个过程就是运行时绑定的过程
所谓运行时绑定就是在编译时候,我知道我要调用的是父类的中的方法,但是程序运行时,我调用的是子类中的方法,也就是说在运行时才确定我调用的是内一个eat方法。
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.
关于重写需要注意的事项:
- 需要重写的方法,不能是被final所修饰的,被final修饰方法是密封的方法,不能被修改
- 需要重写的方法,不能是被private所修饰的,虽然我们在使用private修饰之后,程序没有报错,但是我们eat方法不能在被调用了。
- 需要重写的方法,子类和父类中重写的方法的访问限定符可以是不一样的,但还是子类当中的访问修饰符的访问权限必须是大于父类的访问权限的。例如:假设父类的修饰词是protected,那么子类的修饰词只能是public或protected。不能是另外两个权限比他小的访问权限修饰符。
普通的方法可以重写,但是static静态的方法不能重写。
重写和重载的区别:
需要注意的是:再重写中要求返回值必须相同,这个返回值可以是协变类型,返回值也会是构成继承关系的。
理解多态
什么是多态?
- 父类引用,引用子类对象
- 父类和子类有重名的覆盖方法
- 通过父类引用代用这个重写的方法的时候,就发生了多态。
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况:
class Shape {
public void draw() {
// 啥都不用干
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
/我是分割线//
// Test.java
public class textdome {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawShape(shape1);
drawShape(shape2);
drawShape(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
//输出结果
//♣
//○
//□
上述例子我们可以发现,我们只调用父类的引用就可以实现很多子类的重写方法。
那么使用多态的好处是什么呢?
使用多态的好处:
1》类的调用者对类的使用成本进一步降低
- 封装的过程就是 提供公有的方法,让类的调用者不需要知道类的实现细节,他只管调用这些公有的方法就好了,降低了代码的管理复杂度。
- 多态就是让类的使用者,连这个类的类型都不知道,只需要知道这个对象具有某个方法就好了。
2》能够降低代码的“圈复杂度”,避免大量的时候用if-else语句来进行判断,什么情况下我该使用哪个方法。
上述的所谓圈复杂度就是if-else的数量。
例如之前画图形的例子,我们要像上述一样去打印不同的图形,如果我们不使用多态的话,就要使用一个for循环,然后再循环里面判断哪一次我们该调用哪一个函数:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
这就很麻烦。
如果我们用多态写的话,我们可以创建一个父类的数组,里面存放这个父类的子类对象, 然后用foreach来访问这个数组中的内容:
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
那为什么一个Shape类型的数组里面能存放其他类型的对象呢?因为此时发生了向上转型。
那么也就是说,一个父类的数组,除了可以存放本类型的对象,还可以存放其子类的对象,如果存放其子类的对象,那么在调用的时候就会发生向上转型。
3》可以拓展性更强了
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低:
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高