运用数据抽象的思想编写代码(定义和使用数据类型,将数据类型的值封装在对象中)的方式称为面向对象编程。
数据类型指的是一组值和一组对值的操作的集合。对象是能够存储任意该数据类型的值的实体,或数据类型的实例。
IDEA安装
安装链接:JetBrains: Essential tools for software developers and teams
根据系统下载安装包,修改安装路径,然后什么也不用选,安装好就可以了。
更改字体
FILE——Settings——Editor——font——23
新建项目
1. 为了在合适的文件夹下新建文件,我们首先要找到文件夹(以JavaProject为例)
可以在Module JDK处找到我们JDK的安装目录(找到根目录即可,不需要bin)。
注意:Module name与root要保持一致,如果文件夹里没有JavaProject文件,那么就会根据路径创造一个。
然后就是在JavaProject中创建一个项目文件夹,我们假设为Hello。
同样的,name与root保持一致,否则会出现错误。
在Hello中的src文件夹里创造Java Class(注意那个Hello.iml不能删除,否则src会失效)。然后起一个名字,在里面书写代码即可。
在Bulid中查看代码运行结果(或者直接点箭头)。
修改菜单字体
把"Use custom font"选上,调整字体即可。
修改主题
修改编码
快捷键
1. 删除当前行,默认是ctrl + Y,可以自己配置为 ctrl +d。
2. 在下一行复制当前行,默认ctrl+d,自己可以配置成 ctrl + alt + 向下箭头(似乎笔记本不行)。
3. 补全代码:alt + /
4. 添加注释:ctrl + /
5. 导入改行需要的类(比如Scanner):先配置auto import,然后使用 alt + enter(默认)。
把两个对号都选上即可。
6. 快速格式化代码 ctrl + alt + L
7. 快速运行程序:默认为 shift + F10,自己定义: alt + R,要注意一点,新写完的程序第一次运行需要手动操作,因为没有这个程序运行的记录,手动运行一次后就可以使用快捷键了。
8. 生成构造器:alt + insert --> constructor --> alt多选(选中的元素将成为参数)
9. 查看一个类的层级关系:ctrl + H
10. 将鼠标把一个方法名拉住,输入 ctrl + B,可以定位到该方法定义的位置。
11. 通过在new后面加 .var 可以自动分配变量名。
12. 生成封装的 Set 和 Get 函数:alt + Insert ---> Getter and Setter ---> 按住 ctrl 多选。
13. 去掉构造器的提示信息: 把"Show parameter hins for"去掉就好了。
14. 生成for循环:XX.for
15. 每次创建类都生成版权信息:在Includes里书写
模板
file -> settings -> editor -> Live templates -> Java:查看有哪些模板快捷键,也可以自己增加模板。常用的比如 main,sout,fori 等。
用右上角的加号添加自己的模板:
在左下角的 define 中,添加作用域。
生成输出语句:
生成for循环:
生成main方法:
包
包的作用
1. 区分相同名字的类,可以在不同的包中定义相同名字的类。
2. 当类很多时,可以很好的管理类(类似Java API文档)。
3. 控制访问范围。
基本语法
package com.hspedu; 其中package为关键字,表示引入包。com.hespedu表示包名。
本质分析
包实际上就是创建不同的文件夹来保存类文件。
创建一个包:
com.xiaoming,其中这个 '.' 表示分级,com为一级目录,xiaoming为二级目录。
举例:
import com.shang.Person;
public class HelloWorld {
public static void main(String[] args) {
Person a = new Person(); //导入的可以直接使用
com.xiaoming.Person b = new com.xiaoming.Person(); //没有导入就要写全名字
a.showName();
b.showName();
}
}
去除 import后自动隐藏
把 "Optimize imports on the fly" 取消即可。
命名
命名规则:
只能包含数字、字母、下划线、小圆点,但不能用数字开头,不能是关键字或保留字。
命名规范:
com.公司名.项目名.业务模块名,全部用小写字母。
com.sina.crm.uese; //用户模块
com.sina.crm.order; //订单模块
com.sina.crm.utils; //工具类
常用包
包的使用细节
1. package语句的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package。
2. import指令放在package的下面,在类定义前面,可以有多句并且没有顺序要求。我们用 import 引入包,主要是为了使用包中的类。不建议使用 * 引入全部的类。
访问修饰符
访问范围
注意子类说的是不同包里的子类,同一个包里的子类是可以访问父类的默认属性的。
注意事项
1. 修饰符可以用来修饰类中的 属性,成员方法以及类。成员方法与属性都符合上图的范围。
2. 只有默认和public才能修饰类,默认类只有在同一个包中才能调用,在不同包中,即使使用了import导入该类,也不能用。而public修饰的类不仅在本包中可以调用,也可以在不同包中进行调用,但是需要import导入类。
封装
简介
封装就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只有通过被授权的操作(方法),才能对数据进行操作。
好处
1. 隐藏实现细节,使用者只需要调用即可。类似的,电视机也是一种封装,用户想要开机等操作,只需要在遥控器上按一下就可以了,不用知道电视机内部进行的复杂操作。
2. 可以对要进行赋值的数据进行检验,保证安全合理。
int age = 12000; //这就是没有封装的坏处,没有对赋值的数据进行检验
封装步骤
1. 将属性进行私有化 private,这样就不能直接修改属性了。
2. 提供一个公共的(public) set方法,用于检验数据并给属性赋值。
public void setXxx(类型 参数名){
//加入数据验证的业务逻辑
属性 = 参数名;
}
3. 提供一个公共的 get方法,用于获取属性的值。
public 数据类型 getXxx(){
return Xxx;
}
封装与构造器
为了防止调用构造器绕过set函数,我们可以把set函数写在构造器中。
public Person(String name,int age,double salary){
this.setName(name);
this.setAge(age);
this.setSalary(salary);
} //在构造器中写入封装方法
继承
基本介绍
继承可以解决代码复用,让我们的编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过extends来声明继承父类即可。
示意图
基本语法
class 子类 extends 父类{}
1. 子类会自动拥有父类定义的属性和方法(当然受访问修饰符的限制)。
2. 父类又叫超类,基类。
3. 子类又叫派生类。
4. 假设A继承B,B继承C,那么C也算A的父类。
注意事项
1. 子类继承了父类所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法去访问。
2. 子类必须调用父类的构造器,完成父类的初始化。
3. 当创造子类对象时,不管使用子类哪个构造器,默认情况下都会去调用父类的无参构造器(相当于有一个默认语句 super())。
public class Pupil extends Person{
double score;
Pupil(){
System.out.println("调用pupil构造器");
}
public static void main(String[] args) {
Pupil pupil = new Pupil(); // 调用子类Pupil的构造器
}
}
发现首先调用的是父类的构造器,然后再调用子类的构造器。
4. 如果父类没有提供无参构造器,则必须在子类的构造器中用super指定使用父类的某个构造器完成对父类的初始化工作,否则编译不会通过。
Person(int age,String name){ //父类的构造函数,有参数
this.age = age;
this.name = name;
}
public class Pupil extends Person{ //继承的子类
double score;
Pupil(){
System.out.println("调用pupil构造器");
}
public static void main(String[] args) {
Pupil pupil = new Pupil(); //直接报错,因为没有调用父类的构造器
}
}
Pupil(){
super(10,"wang");
System.out.println("调用pupil构造器");
} //正确,用super调用父类构造器
5. 如果希望指定去调用父类的某个构造器,则显式的调用一下:super(参数列表)。
6. super在使用时,必须放在构造器第一行( super() 调用构造器这种方法只能在构造器中使用)
Pupil(){
System.out.println("调用pupil构造器");
super(10,"wang"); //报错,super调用构造器必须放在第一行
}
7. super() 和 this() 都是调用构造器的方法,都必须放在构造器的第一行,因此这两个方法不能共存在一个构造器。
8. Java中所有类都是Object类的子类(可以用ctrl + H查看)。
9. 调用子类构造器时,会向上一直调用父类的构造器,一直追溯到Object类。
public static void main(String[] args) {
Pupil pupil = new Pupil(); //从上往下开始调用构造器
}
10. 子类最多只能继承一个父类(直接继承,删除了C++的多重继承),Java中是但继承机制。那么任何让A类同时继承B类和C类? A 继承 B,B 继承 C。
11. 不能滥用继承,子类和父类之间必须满足 “is - a”(也就是必须有共同点)的逻辑关系。
Music extends Person //不合理
Cat extends Animal //合理
本质分析
分析当子类继承父类,创建子类对象时,内存中到底发生了什么。
Son son = new Son();
System.out.println(son.name); //本类中有该信息,返回 "小头儿子"
System.out.println(son.age); //父类中有该信息,返回 39
System.out.println(son.hobby); //父类的父类有该信息,返回 "旅游"
要按照查找关系来返回信息:
1. 首先看子类是否有该属性,如果子类有这个属性,则返回信息(因为是在本类中,因此绝对可以访问,即使是private)。
2. 如果子类没有这个属性,就看父类有没有这个属性,如果父类有该属性,并且可以访问,就返回信息,如果该信息不可访问(private),那么就直接报错(can not access),不会再向上查找了。
3. 如果父类没有该信息,那么就按照 (2) 的规则,继续找上级,直到Object,如果到最顶端也没有查找到,则提示该方法不存在。
super关键字
基本介绍
super代表父类的引用,用于访问父类的属性、方法、构造器。
基本语法
1. 访问父类的属性,但不能访问父类的private属性:super.属性名;
2. 访问父类的方法,但不能访问父类的private方法:super.方法名(参数列表);
3. 访问父类的构造器(前面介绍过):super(参数列表); 只能放在构造器的第一句,只能出现一句
细节/好处
1. super调用父类构造器,可以使分工明确。父类属性由父类初始化,子类的属性由子类初始化。
2. 当子类中由和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果(只不过this和直接访问是从本类开始找,super是从父类开始找)。 调用类的属性的规则在上面已经说明过了,调用类的方法的规则与其相同,只不过把属性换成了方法。
子类确实可以写一个与父类完全相同的方法,虽然左边会有隐患提示,但是编译没有问题。
下面对于子类调用父类的方法举一个例子(属性因为很简单就不举例了)
定义一个父类Person:
public class Person { //有四个不同修饰符的方法
public void showName1(){
System.out.println("调用Person的public方法");
}
void showName2(){
System.out.println("调用Person的无修饰符方法");
}
protected void showName3(){
System.out.println("调用Person的protected方法");
}
private void showName4(){
System.out.println("调用Person的private方法");
}
}
定义一个子类Student,先测试在main方法中调用方法(必须用声明的变量调用):
public class Student extends Person{
public static void main(String[] args) {
Student student = new Student();
student.showName1(); //成功调用
student.showName2(); //成功调用
student.showName3(); //成功调用
student.showName4(); //报错,不能调用父类的private方法
}
}
然后在子类的方法中继续测试,由于 this 和 super 都不能在主方法中调用,因此声明一个test方法,在该方法中测试this和super。
public void test(){
this.showName1(); //成功调用
this.showName2(); //成功调用
this.showName3(); //成功调用
this.showName4(); //报错,不能调用private方法
super.showName1(); //成功调用
super.showName2(); //成功调用
super.showName3(); //成功调用
super.showName4(); //报错,不能调用private方法
}
当然,该方法还是需要用main方法中声明的变量调用。
同样的,最重要的一点——在子类调用一个方法时,如果子类没有该方法,会一直向上寻找,如果找到Object类还没有找到,就会报错(super直接忽略本类)。如果在中间找到了一个private的方法,即使上面还有该方法,也不会再寻找了,而是直接报错。
举一个例子:
private void showName1(){ //在Student类中加入一个与Person类同名但不同修饰符的函数
System.out.println("调用Student的private函数");
}
public class Pupil extends Student{ //继承Student
public static void main(String[] args) {
Pupil pupil = new Pupil();
pupil.showName1(); //直接报错,寻找到Student就停了,而不是继续向Person寻找
}
}
3. 如果多个父类中有相同的成员,那么super的访问根据就近原则。
4. super和this的区别
方法重写/覆盖(override)
基本介绍
方法覆盖就是子类有一个方法和父类的某个方法 名称、返回类型、参数一样,这样我们就说子类的这个方法覆盖了父类的那个方法。
使用细节
1. 子类方法的 形参列表,方法名称 要和父类的完全一样。
2. 子类方法的 返回类型 可以和父类方法的返回类型一样,或者是父类返回类型的子类。
public Object showName(){
System.out.println("返回值为Object的父类方法");
return "OK";
} //父类的方法
public String showName(){
System.out.println("返回值为String的子类方法");
return "OK";
} //子类的方法
public static void main(String[] args) {
Student student = new Student();
student.showName(); //调用
}
public String showName(){
System.out.println("返回值为Object的父类方法");
return "OK";
}
public Object showName(){
System.out.println("返回值为String的子类方法");
return "OK";
} //报错
3. 子类方法不能缩小父类方法的访问权限(但扩大可以):public > protected > 默认 > private
public void showName(){
System.out.println("修饰符为public的父类方法");
}
protected void showName(){
System.out.println("修饰符为protected的子类方法");
} //报错,减小了范围
4. 重载和重写的比较
多态
对于解决某些问题,比如 假设需要创建一个方法给宠物喂食,那么用传统方法来说,给猫喂鱼,狗喂骨头等等,需要每一个都写一个方法,这样导致代码的复用性不高,而且不利于代码的维护,因此需要多态。
多态指 方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
方法的多态
重写和重载就体现了多态。
// 方法重载体现了多态——在同类中方法有多种形态
public int sum(int num1,int num2){
return num1 + num2;
}
public int sum(int num1,int num2,int num3){
return num1+num2+num3;
}
// 方法重写体现了多态——在不同类中方法也有多种形态
public int sum(int num1,int num2)
{
return num1 + num2;
} //父类方法
public int sum(int num1,int num2)
{
return num1 + num2;
} //子类方法
对象的多态
1. 一个对象的编译类型和运行类型可以不一致。编译类型看定义时 =号 的左边,运行类型看 =号 的右边。
Animal animal = new Dog(); // animal 编译类型是Animal,运行类型为 Dog
2. 编译类型在定义对象时就确定了,不能改变。
3. 运行类型是可以变化的。
animal = new Cat(); // animal的运行类型变成了Cat,编译类型仍然是Animal
4. 对象多态的前提:两个对象(类)之间存在继承关系。
因为对象的多态,很多操作就简洁许多了。以上面的喂食例子,我们正常写的话应该是这样:
public void feed(Cat cat, Fish fish){
...; //输出喂食信息
}
public void feed(Dog dog, Fish fish){
...; //输出喂食信息
}
...... //所有动物类与食物类都要写一遍,写在Master类中
而如果用对象的多态方法来写,则是下面这样:
public void feed(Animal animal, Food food){
...;
} // 只需在Master类中写一个即可
当调用时,仍然是传入Animal类和Food类的子类,这时就用到了多态机制。
5. 多态参数:方法定义的形参类型为父类类型,实参类型允许为子类类型。
向上转型
1. 本质:父类的引用指向了子类的对象。
2. 语法:父类类型 引用名 = new 子类类型();
3. 特点:
1)编译类型看左边,运行类型看右边。
2)可以调用父类中的所有成员(当然前提是遵守访问权限)。
3)不能调用子类中特有成员(如果子类重写了父类的方法,或者子类有和父类一样的属性,那么由于动态绑定机制,调用父类的成员和方法会先从子类找,因此子类重写的方法和相同的属性是可以被调用的),因为在编译阶段,能调用哪些成员,是由编译类型来决定的。
Animal animal = new Cat();
animal.catchMouse(); //错误,因为catchMouse是子类的特有方法,向上转型不能调用
4)最终运行效果看子类的具体实现,即调用方法时,从子类(运行类型)开始查找方法,规则与前面的方法调用规则一致。
向下转型
1. 语法:子类类型 引用名 = (子类类型) 父类引用。
Cat cat = (Cat)animal;
这里注意一点,向下转型后,cat的编译类型和运行类型都是Cat,cat和animal都指向Cat的对象(注意animal没有消失)。
2. 只能强转父类的引用,不能强转父类的对象。父类的对象是创建在堆区中的,这个是不能改变的(因为已经创建了),但是可以改变指向该对象的指针,向下转型就是让它指向一个子类的对象。
3. 要求父类的引用必须指向的是当前目标类型的对象。
Animal animal = new Cat();
Dog dog = (Dog)animal; //报错
Cat cat = (Cat)animal; //正确
就比如这个例子,父类的引用本来就指向Cat类的对象,因此向下转型只能使用Cat。按照错误语句的理解,让一只狗指向猫对象,那肯定是错误的。
4. 当向下转型后,可以调用子类类型中所有的成员(当然要符合访问范围)。
5. 下面是错误的向下转型写法,不能让没有引用的对象进行向下转型。
Cat cat = (Cat)(new Animal());
Cat cat = (Cat)new Animal();
// 这两种写法都是错误的,不能让没有引用的对象进行向下转型
// 编译器报告 cannot be cast to 错误
Java的动态绑定机制
1. 当调用对象方法的时候(不管是直接调用还是方法里调用),该方法会和该对象的内存地址(运行类型)绑定,根据运行类型进行调用,如果没有该方法,就启用继承机制。
2. 当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性,如果没有就启用继承机制。
用一个例子解释清楚
public class Computer {
public int i = 10;
public int sum(){
return getI() + 10; //注意这里进行了方法的调用
}
public int sum1(){
return i + 10;
}
public int getI(){
return i;
}
}
public class NotePad extends Computer{
public int i = 200;
public int sum(){
return i + 100;
}
public int getI(){
return i;
}
public int sum1(){
return i + 200;
}
}
父类和子类的成员完全相同,只不过内容有变化,注意sum不是子类的特有方法,因此向上转型可以调用。
public static void main(String[] args) {
Computer a = new NotePad(); //向上转型,运行类型为 NotePad
System.out.println(a.sum()); //输出 300,用的子类的方法和子类的i
System.out.println(a.sum1()); //输出 400
System.out.println(a.i); //输出 10,为父类的i
}
可以发现,由于运行类型是NotePad,当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定,因此调用的方法直接从子类开始找,由于当在方法中使用对象属性时,没有动态绑定机制,调用哪个类的方法,就用哪个类的属性。子类的sum方法用到了 i,根据这条定理,应该用子类的 i。但是注意一点,如果直接调用a的i,由于属性没有重写,调用属性要看编译类型,因此使用父类的 i。
然后把子类的sum方法和sum1方法全部删除。
public class NotePad extends Computer{
public int i = 200;
public int getI(){
return i;
}
}
public class Test {
public static void main(String[] args) {
Computer a = new NotePad();
System.out.println(a.sum()); //输出210,父类的sum方法调用的是子类的getI方法
System.out.println(a.sum1()); //输出20,用父类的i
}
}
当把子类的sum方法和sum1方法都删除后,调用这些方法就要启动继承机制了,在父类中寻找这两个方法。注意父类的sum方法调用了一个 getI 函数,在方法里调用的方法也和运行类型绑定,因此调用的是子类的getI方法,返回的是子类的i,因此结果为210,而父类的sum1方法调用了属性 i,由于属性没有动态绑定机制,因此调用自己的 i,结果为20。
注:属性没有重写之说,调用哪个属性要看编译类型。
public class Computer {
String name = "我是一台电脑"; //在父类定义一个属性
}
public class PC extends Computer{
String name = "我是一台PC"; //在子类定义一个相同名字的属性
}
public class Test {
public static void main(String[] args) {
Computer c1 = new Computer(); //编译类型为Computer
Computer c2 = new PC(); //向下转型,编译类型为Computer
PC c3 = new PC(); //编译类型为PC
PC c4 = (PC)new Computer(); //向上转型,编译类型为PC
System.out.println(c1.name);
System.out.println(c2.name);
System.out.println(c3.name);
System.out.println(c4.name);
}
}