计算机基础复习
ref:www.liaoxuefeng.com/wiki/125259…
内存的基本结构
内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit。它的二进制表示范围从00000000255,换算成十六进制是11111111,换算成十进制是000~ff。
内存单元从0开始编号,称为内存地址。每个内存单元可以看作一间房间,内存地址就是门牌号。
0 1 2 3 4 5 6 ...
┌───┬───┬───┬───┬───┬───┬───┐
│ │ │ │ │ │ │ │...
└───┴───┴───┴───┴───┴───┴───┘
一个字节是1byte,1024字节是1K,1024K是1M,1024M是1G,1024G是1T。一个拥有4T内存的计算机的字节数量就是:
4T = 4 x 1024G
= 4 x 1024 x 1024M
= 4 x 1024 x 1024 x 1024K
= 4 x 1024 x 1024 x 1024 x 1024
= 4398046511104
不同的数据类型占用的字节数不一样。我们看一下Java基本数据类型占用的字节数:
┌───┐
byte │ │ -128 ~ 127
└───┘
┌───┬───┐
short │ │ │
└───┴───┘
┌───┬───┬───┬───┐
int │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
long │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┬───┬───┐
float │ │ │ │ │
└───┴───┴───┴───┘
┌───┬───┬───┬───┬───┬───┬───┬───┐
double │ │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘
┌───┬───┐
char │ │ │
└───┴───┘
byte恰好就是一个字节,而long和double需要8个字节。
byte,short、int、long 都是整形,java 只有有符号整形,它们能表示的范围如下:
- byte:-128 ~ 127
- short: -32768 ~ 32767
- int: -2147483648 ~ 2147483647。对于各种 id 来说,不太够。所以代码里的 memberId、xxId 一般都用long。
- long: -9223372036854775808 ~ 9223372036854775807
常用基础语法
ref:wizardforcel.gitbooks.io/modern-java…
基本类型、引用类型
Java的数据类型分两种:
- 基本类型:
byte,short,int,long,boolean,float,double,char - 引用类型:所有
class和interface类型
传递区别
-
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
-
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。要注意不可变类型(比如String)修改后是产生了新的对象,会出现「一方修改后,不影响另一方」的幻觉。
类型自动提升与强制转型
short + int = int
int i = 12345;
short s = (short)i;
浮点数计算误差
todo
格式化输出
JDK文档java.util.Formatter
== 和 equals
不可变类型:
- String
引用类型:
相当于指针,可指向
null
等待一会儿
TimeUnit.SECONDS.sleep(1);
// 或
Thread.sleep(1000)
定义变量,可用 var 省略变量类型,编译器会根据赋值语句自动推断类型
var sb = new StringBuilder();
// 编译时会处理为:
StringBuilder sb = new StringBuilder();
List
长度不可变list
// 长度不可变的数组:T[],比如 int[]、float[]:
int[] ns = new int[5];
ns[1] = 2;
// 或定义时初始化:
int[] ns = new int[] { 68, 79, 91, 85, 62 };
// 或
int[] ns = { 68, 79, 91, 85, 62 };
// 求长度
System.out.println(ns.length);
如果执行 ns = new int[] { 1, 2, 3 }; ,ns 实际指向了新的内存地址,并不是修改了原数组的长度。
长度可变
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
遍历
int[] ns = { 1, 4, 9, 16, 25 };
for (int i=0; i<ns.length; i++) {
int n = ns[i];
System.out.println(n);
}
// 或
for (int n : ns) {
System.out.println(n);
}
// 或
import java.util.Arrays;
System.out.println(Arrays.toString(ns));
二维数组
int[][] ns = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
三维数组
int[][][] ns = {
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
},
{
{10, 11},
{12, 13}
},
{
{14, 15, 16},
{17, 18}
}
};
定义list排序
// 注:b.compareTo(a) 是 String 内置的比较器,实际可以自己另外定义。
//旧:
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
// java8 可简化为:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
// 继续简化:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
// 继续简化:
Collections.sort(names, (a, b) -> b.compareTo(a));
可变参数 vs list 参数
public void setNames(String... names) {
this.names = names;
}
public void setNames(String[] names) {
this.names = names;
}
可变参数有些优势:
- 可以保证无法传入
null,因为传入0个参数时,接收到的实际值是一个空数组而不是null - 调用方不用构造 list,使用方便
面向对象
class
一个Java源文件(xx.java)可以包含多个类的定义,但只能定义一个public类,且public类名必须与文件名一致。如果要定义多个public类,必须拆到多个Java源文件中。
定义class中不变的属性
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
//...
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
//...
}
field
类的数据成员,即字段
method
类的方法成员
setter 易于校验参数
overload-重载
函数名相同,参数不同
override-覆写
函数签名(出入参、函数名)相同,可覆盖父类方案,可以添加 @Override,让编译器帮助检查是否进行了正确的覆写。
继承
子类自动获得了父类的所有字段,严禁定义与父类重名的字段。
术语
- 超类(super class),父类(parent class),基类(base class)
- 子类(subclass),扩展类(extended class)
只有 Object类没有父类,其他类都有且只有一个父类。对 Object 类的继承一般是隐式的。
private vs protected
子类无法访问父类的private字段或者private方法,这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private改为protected。用protected修饰的字段可以被子类访问。
因此,protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问
super
编译器会默认给子类的构造方法添加 super(),但如果父类没有无参数的构造方法,就需要显式调用:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用
如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override:
final 组织继承
final 的 class 不能被继承
向上转型
Person p = new Student(); 是可以的,因为 new Student()这个实例拥有p这个引用变量(类型为Person)的所有功能。这叫向上转型upcasting。可以继续对p执行 Object o = p。
向下转型不一定可以。
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
需要先instance of
Person p = new Student();
if (p instanceof Student) {
// 只有判断成功才会向下转型:
Student s = (Student) p; // 一定会成功
}
组合
一般用于 has a。
class Student extends Person {
protected Book book;
protected int score;
}
多态
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。例如:
Person p = new Student();
p.run(); // 无法确定运行时究竟调用哪个run()方法
有童鞋会问,从上面的代码一看就明白,肯定调用的是Student的run()方法啊。
但是,假设我们编写这样一个方法:
public void runTwice(Person p) {
p.run();
p.run();
}
它传入的参数类型是Person,我们是无法知道传入的参数实际类型究竟是Person,还是Student,还是Person的其他子类,因此,也无法确定调用的是不是Person类定义的run()方法。
多态具有一个非常强大的功能,就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
抽象类 vs 接口
抽象类
如果一个class定义了方法,但没有具体执行代码,这个方法就是抽象方法,抽象方法用abstract修饰。
因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
使用abstract修饰的类就是抽象类。我们无法实例化一个抽象类:
abstract class Person {
public abstract void run();
}
Person p = new Person(); // 编译错误
无法实例化的抽象类有什么用?
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现(箱单雨一种覆写,可以用override)其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。
比如
Person s = new Student();
Person t = new Teacher();
// 同样不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
- 上层代码只定义规范(例如:
abstract class Person); - 不需要子类就可以实现业务逻辑(正常编译);
- 具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法,就可以把该抽象类改写为接口。
所谓interface,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是public abstract的,所以这两个修饰符不需要写出来(写不写效果都一样)。实现类可以不必覆写default方法。
default方法和抽象类的普通方法是有所不同的。因为interface没有字段,default方法无法访问字段,而抽象类的普通方法可以访问实例字段。
interface Person {
void run();
String getName();
}
class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + " run");
}
@Override
public String getName() {
return this.name;
}
}
如果定义好了接口,但希望给所有子类添加实现相同的方法,可以用default。
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}
对比
注意,一个类可以实现多个interface,但只能继承一个类。
抽象类和接口的对比如下:
| abstract class | interface | |
|---|---|---|
| 继承 | 只能extends一个class | 可以implements多个interface |
| 字段 | 可以定义实例字段 | 不能定义实例字段 |
| 抽象方法 | 可以定义抽象方法 | 可以定义抽象方法 |
| 非抽象方法 | 可以定义非抽象方法 | 可以定义default方法 |
类继承接口用implements,接口继承接口用extends,相当于扩展接口的方法。
继承的设计(用不利索)
合理设计interface和abstract class的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract class中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
匿名类
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
Runnable本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable接口的匿名类,并且通过new实例化该匿名类,然后转型为Runnable。在定义匿名类的时候就必须实例化它,定义匿名类的写法如下:
Runnable r = new Runnable() {
// 实现必要的抽象方法...
};
匿名类和Inner Class一样,可以访问Outer Class的private字段和方法。之所以我们要定义匿名类,是因为在这里我们通常不关心类名,比直接定义Inner Class可以少写很多代码。
static
静态字段
www.liaoxuefeng.com/wiki/125259…
对于静态字段,无论修改哪个实例的静态字段,效果都是一样的:所有实例的静态字段都被修改了,原因是静态字段并不属于实例:
┌──────────────────┐
ming ──>│Person instance │
├──────────────────┤
│name = "Xiao Ming"│
│age = 12 │
│number ───────────┼──┐ ┌─────────────┐
└──────────────────┘ │ │Person class │
│ ├─────────────┤
├───>│number = 99 │
┌──────────────────┐ │ └─────────────┘
hong ──>│Person instance │ │
├──────────────────┤ │
│name = "Xiao Hong"│ │
│age = 15 │ │
│number ───────────┼──┘
└──────────────────┘
因为interface是一个纯抽象类,所以它不能定义实例字段。但是,interface是可以有静态字段的,并且静态字段必须为final类型,且public static final 关键字可以省略
静态方法
调用静态方法则不需要实例变量,通过类名就可以调用。
因为静态方法属于class而不属于实例,因此,静态方法内部,无法访问this变量,也无法访问实例字段,它只能访问静态字段。
静态方法经常用于工具类。例如:
- Arrays.sort()
- Math.random()
静态方法也经常用于辅助方法。注意到Java程序的入口main()也是静态方法。
package
用于隔离命名空间、作用域等。
包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
作用域:不用public、protected、private修饰的字段和方法就是包作用域.
命名注意不要和java.lang包的类重名,即自己的类不要使用String、System、Runtime这些名字。因为默认自动import java.lang.*。
JavaBean
只有field及其读写方法的class 就是javaBean,类似于贫血模式中的一种数据模型。
JavaBean一定要实现Serializable接口实现序列化吗?Serializable序列化到底是什么意思呢???
泛型
<T> T 与 T
区别在于一个函数的入参类型是函数层面的自由如风;还是只是类实例化时限定入参类型,只在类这里自由。
<T> 与 <?>
没太看懂,遇到的时候再说。