java 面向对象基础
内存图
成员变量和局部变量的区别
- 在类中的位置不同
- 成员变量在类中, 在方法外
- 局部变量在方法内或方法声明中(形参)
public class Phone {
String x; //成员变量
public void showPhone() {
int y; //局部变量
}
}
- 内存中位置不同
- 成员变量在堆内存
- 局部变量在栈内存
- 生命周期不同
- 成员变量随着对象的消失而消失
- 局部变量随着方法的消失而消失
- 初始化值不同
- 成员变量有默认值
- 局部变量没有默认值, 必须先赋值后使用
常用API
nextLine
获取键盘输入的字符串
用法
nextLine属于Scanner类, 所以在使用nextLine时需要先创建Scanner实例, 然后调用Scanner实例的nextLine方法
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
String构造方法
-
String(String original)
把字符串封装成字符串对象 -
String(char[] value)
把字符数组封装成字符串对象 -
String(char[] value, int offset, int count)
把字符数组中从索引offset开始的count个字符封装成字符串对象
直接赋值String和new String()的区别
String成员方法
判断
-
boolean equals(Object object)
比较字符串内容是否相同 -
boolean equalsIgnoreCase(String str)
比较字符串内容是否相同, 忽略大小写 -
boolean startsWith(String str)
判断字符串是否以指定str开头 -
boolean endsWith(String str)
判断字符串是否以指定的str结尾
获取
-
int length()
获取字符串长度 -
char charAt(int index)
获取index位置的字符 -
int indexOf(String str)
获取str在字符串中第一次出现的位置 -
String substring(int start, int end)
从start位置开始截取字符串到end位置(不包括end), 没有给end值则默认截取到最后
StringBuilder类
先看下面的图
当我们对字符串拼接时, 会把拼接的字符串放在方法区常量池的一个新的地址中, 并且我们的变量指向这个新的地址, 这就意味着拼接前的字符串在拼接之后就变成了内存中的垃圾
StringBuilder是一个可变的字符串, 可以在其后随意拼接字符串, 这样我们在拼接字符串时, 就会动态的添加到原字符串后面, 这样就不会造成内存垃圾了
StringBuilder构造方法
-
StringBuilder()
创建一个StringBuilder类 - `StringBuilder(String str)
将字符串str转换为StringBuiler类型
StringBulider成员方法
-
public int capacity()
返回当前容量 -
public int length()
返回当前长度 -
public StringBuilder append(任意类型)
拼接并返回StringBuilder本身 -
public StringBuilder reverse()
反转并返回StringBuilder本身 -
public String toString()
将StringBuilder转换成一个新的String类型字符串
将数组转换成字符串
public static void main(String[] args) {
int[] arr = {1,2,3};
StringBuilder sb = new StringBuilder();
for(int i = 0; i < arr.length; i++) {
sb.append(arr[i]);
}
String newString = sb.toString();
System.out.println(newString);
}
ArrayList
ArrayList数组列表可以动态的创建数组, 并且封装了很多好用的数组成员方法, 可以让我们很方便的操作数组
构造函数
ArrayList<T>()
ArrayList<String> arr = new ArrayList<String>(); //创建以字符串为元素的ArrayList实例
成员方法
-
public boolean add(T t)
添加元素 -
public void add(int index, T t)
在index位置添加元素
static和代码块
static关键字
static修饰的静态方法/属性属于类, 所有该类的实例都可以调用, 其随着类而加载, 优先于对象加载
下面的代码, 我们在Student类中定义了一个static属性school, 我们在Student的实例对象p1中并没有给school赋值, 但是我们却可以直接调用p1.school
public class MyPhone {
public static void main(String[] args) {
Student p1 = new Student();
p1.name = "小张";
p1.age = 20;
System.out.println(p1.name + ' ' + p1.age + ' ' + p1.school);
}
}
class Student {
public static String school = "清华";
public String name;
public int age;
}
- 由于静态方法和静态属性都是随着类的加载而加载的, 所以静态方法中可以调用静态属性和静态方法
- 静态方法中不能调用普通成员变量和成员方法
- 静态方法中没有this, 因为静态方法是随类加载的, 所以此时还没有对象
static的应用举例
Math类就是static的典型应用, Math类中的所有方法都是静态方法, 所以我们直接用Math类调用方法就可以了, 不需要创建Math实例, 并且也无法创建Math实例
继承与抽象类
继承就是让其他类拥有某个类中的成员变量和方法, 这个被继承的类就叫做父类
我们用extends关键字来继承类
看下面的代码, 我们让Man继承了Person, 所以我们在创建Man的实例对象时, 我们不但有Man的成员变量agent, 同时还拥有了继承自Person类的name, age属性, 以及say方法, 这样就可以大大减少代码量
class Person {
String name;
int age;
public void say() {
System.out.println("hello");
}
}
class Man extends Person {
String agent;
}
java是单继承语言
- 一个类只能有一个父类
- 一个父类可以有多个子类
- java可以多层继承, 也就是说一个类可以继承其父类的父类的属性和方法
java继承中成员变量的特点
- 子类只能继承父类非私有成员变量
- 继承也符合变量就近原则, 子类中如果有跟父类的同名变量, 则优先使用子类中的成员变量
- 可以用
super.成员变量
来调用父类的同名变量
java继承中成员方法的特点
- 方法的重写: 子类中如果有跟父类完全相同的成员方法, 则会覆盖父类的成员方法, 在子类的实例中会优先调用子类的同名方法, 如果想调用父类的同名方法,依然使用
super
关键字 - 当父类的某个方法不能满足子类的要求时, 我们可以通过重写来扩展该方法, 此时只需要在子类的同名方法中用super调用父类同名方法就可以实现在子类中对该父类方法的功能扩展
- 在子类重写的放上前加入
@Override
, 这样在重写时如果有错误ide会提示错误 - 不能重写父类的私有成员方法
class Person {
String name;
int age;
public void say() {
System.out.println("hello");
}
}
@Override
class Man extends Person {
public void say() {
super.say();
System.out.println("world");
}
}
java继承构造方法的执行顺序
- 在有子父类继承关系的类中, 创建子类对象时,调用子类的构造方法, 如果子类构造方法的第一行代码没有调用父类的构造函数, 则会默认的调用父类的无参构造
- 如果想手动调用父类的构造方法, 直接在子类构造方法中的第一行使用
super(参数)
class Person {
String name;
int age;
public Person() {
}
public void say() {
System.out.println("hello");
}
}
class Man extends Person {
public Man() {
super();
}
public void say() {
super.say();
System.out.println("world");
}
}
- 可以在子类的构造方法中的第一行使用了
this(参数)
调用子类的其他构造方法, 调用顺序是:- 先进入this调用的子类构造函数
- 判断第一行是手动调用还是默认调用父类的构造函数, 执行父类构造函数
- 执行this调用的子类构造函数
- 执行调用this的子构造函数
其实这种调用顺序是有原因的, 因为子类继承父类, 所以子类很可能要调用父类中的成员变量或者成员方法, 所以在子类调用之前预先调用父类就是为了如果子类需要用的父类的某些成员变量或者方法前, 先将其初始化
抽象类和抽象方法
用abstract
修饰的类和成员方法就是抽象类和抽象方法
abstract class Teacher { // 抽象类
public abstract void say(); // 抽象方法
}
- 抽象方法只能存在于抽象类中
- 抽象类中可以有非抽象方法
- 抽象类不能实例化
- 继承抽象类的普通类必须重写抽象类中的所有抽象方法
- 抽象类中可以有成员变量和常量
抽象类举例
下面我们抽象一个老师的类
public class MyTeacher {
public static void main(String[] args) {
BasicTeacher bt = new BasicTeacher();
bt.name = "Adam";
bt.age = 20;
bt.gender = "male";
bt.teach();
}
}
abstract class Teacher {
String name; // 姓名
int age; // 年龄
String gender; // 性别
public abstract void teach();
}
class BasicTeacher extends Teacher {
@Override
public void teach() {
System.out.println("基础班课程");
}
}
接口
接口的出现主要是为了结局单一继承的局限性
接口比抽象类更抽象, 它只能有抽象方法
接口可以通过类来实现
nterface Animal {
public abstract void eat();
}
class Cat implements Animal {
public void eat() {
System.out.println("吃鱼");
}
}
接口的特点
- 接口中所有的方法都是抽象方法无论你是否写abstract关键字
- 实现接口必须要将其所有抽象方法都具现
- 接口内只能定义公共静态常量
- 一个类可以实现多个接口
- java 9+可以在接口中使用静态方法、默认方法、私有方法、私有静态方法、常量
- 默认方法、静态方法、私有方法、私有静态方法都必须在接口中实现
多态
多态的前提条件
- 子父类继承关系
- 方法的重写
- 父类引用指向子类对象
看上面的条件可能看不懂, 实际上总的来说就是, 一个子类重写了父类的方法, 然后我们定义了一个父类类型的变量, 然后把这个变量用子类的实例赋值, 看下面的例子
public class MyAnimal {
public static void main(String[] args) {
Animal a = new Cat(); // 多态
a.eat();
}
}
class Animal {
public void eat() {
}
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
- 我们的Cat类继承了Animal, 并且在Cat类中重写了Animal的eat成员方法
- 我们在main方法中声明了一个Animal类型的变量a, 并且将它指向一个Cat实例
- 我们调用a的eat方法, 此时调用的是Cat的eat方法
这就是典型的多态例子
多态的成员变量及方法
- 由于成员变量没有重写和动态绑定的概念, 所以多态中调用的成员变量为定义类的成员变量, 而不是实例对象的成员变量, 比如上面的例子, 如果我们需要调用成员变量, 那么这个变量来自于Animal而不是Cat
- 对于重写过的普通成员方法, 多态调用的是重写后的成员方法, 比如上面的例子普通成员方法则会调用Cat实例的成员方法, 而对于重写过的静态方法则会调用Animal的静态方法, 原因是因为静态方法相当于直接调用类中的方法而非实例中的方法
多态的优缺点
缺点
- 无法直接访问子类特有的成员变量
优点
- 提高代码的扩展性和维护性
内部类
分类
- 成员内部类
- 局部内部类
- 匿名内部类
成员内部类
所在位置和成员变量和成员方法一样, 同样可以使用修饰符修饰。下面的例子我们在Outer类中间创建了一个内部类:
class Outer {
private int num = 10;
public void show() {
Inner in = new Inner(); //创建内部类实例对象
in.methods(); //调用实例的methods方法, 此时num调用的是Outer的num
System.out. println("Hello");
}
// 内部类
public class Inner {
public void methods() {
System.out.println(num);
}
}
}
- 成员内部类可以直接访问外部类的成员包括私有成员, 因为我们可以把内部类看成外部类的一个特殊成员变量
- 在相同包的其他类中若想创建该外部类的内部类实例, 可以import内部类或者按如下方法创建实例
Outer.Inner in = new Outer().new Inner();
- 我们也可以用static修饰符修饰内部类, 修饰后内部类的性质跟静态成员变量/方法无异
局部内部类
局部内部类是定义在成员方法中的内部类
class Outer {
private int num = 10;
public void show() {
// 局部内部类
class Inner {
private int num2 = 10;
public void methods() {
}
}
Inner in = new Inner();
in.methods();
}
}
- 局部内部类的作用域跟局部变量是一样的, 只能是方法内, 所以不能在方法外创建和使用局部内部类的实例对象
- 局部内部类应用场景有限, 一般用的不太多
匿名内部类
定义在方法内部的没有类名的内部类, 匿名内部类实际上是对某个类的继承或对某些接口的实现, 所以在使用匿名内部类时, 必须预先定义其父类或接口
class Outer {
private int num = 10;
public void show() {
// 匿名内部类
new Inner() {
public void function() {}
};
}
}
//匿名内部类实现的接口
interface Inner {
public void function();
}
我们还可以利用多态把匿名内部类赋值给类型为匿名内部类的父类的变量来实现匿名内部类, 如:
Inner i = new Inner(){
public void function() {}
};
泛型
泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。
泛型类
假设我们创建以下Box类:
class Box{
private String object;
public void set(String str) {
this.object = str;
}
public String get(){
return object;
}
}
Box有一个非常明显的局限, 只能传String类型的成员变量, 当我们要传int类型的成员变量时, 就必须要重写Box, 这要那个Box的复用性就很差了
class Box<T>{
private T object;
public void set(T t) {
this.object = t;
}
public T get(){
return object;
}
}
这样我们的Box类便可以得到复用,我们可以将T替换成任何我们想要的类型:
Box<Integer> integerBox = new Box<Integer>();
Box<Double> doubleBox = new Box<Double>();
Box<String> stringBox = new Box<String>();
泛型方法
明一个泛型方法很简单,只要在返回类型前面加上一个类似<K, V>的形式就行了:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
我们可以像这样调用泛型方法
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
多线程
- 进程
一个应用程序在内存中的执行区域 - 线程
进程中的一个执行控制单元/执行路径, 一个进程中有一个线程我们叫做单线程, 一个进程中有多个线程我们叫做多线程 - 单线程
一个只做一件事情, 安全性高, 效率低 - 多线程
同时做多件事情, 安全性低, 效率高