1. 基本介绍

1.1 Java是什么

应用领域:

  • Java SE: 是本课程的核心,也是java语言的核心,可用来开发桌面软件(现在很少用这个功能)
  • Jave ME: 开发功能机小游戏,已淘汰
  • Jave EE: 用来开发服务端程序,奠定了Java的地位(最广泛的应用领域)
  • Android: 开发智能机程序
  • 大数据: 首选Java开发,升华普通数据

运行机制:
前提: 程序员编写的是源代码,计算机最终运行的是机器码。
如何生成机器码:

  1. 编译型: 程序运行前,由编译器将源代码完全翻译成机器码。
  2. 解释型: 程序运行前,由解释器将源代码逐行翻译成机器码,并运行。

编译
Java采用的方式: 混合型,先由编译器完全翻译成字节码,再由解释器逐行翻译成机器码,并运行。

为什么: 在90年代,受制于硬件的限制,编译型语言的速度快于解释型语言,但是编译型语言对于跨平台(不同的架构)要花额外的精力去修改,因此Java既有编译型语言运行快的优点,也有解释型语言“一次编写,到处运行”的优势。

从图上可以看出,不同架构的机器有自己的解释器,因此对程序员更友好,不用考虑太多跨平台的问题。

java运行机制

1.2 Java Kit开发工具

  • JDK(Java Development Kit): 开发工具包,编写Java程序
  • JRE(Java Runtime Environment): Java运行环境,用户基本运行程序的必要
    JDK

1.3 第一个Java程式

  • 编写源文件: 后缀.java
  • 编译Java源代码: javac First.java 得到字节码文件First.class
  • 运行Java字节码: java First 程序运行
  • Java虚拟机负责解释运行class文件

2. 变量

2.1 什么是变量

数据类型用于指导计算机,该为这块内存分配多大的空间。
使用变量时:

  1. 声明变量, int a;
  2. 初始化变量, a = 10;
  3. 使用变量, a = 200

需要注意;

  1. 未声明的变量不能使用
  2. 为初始化的变量不能使用
  3. 存入的数据必须符合声明的类型

2.2 基本数据类型

基本数据类型和它们的范围

类型 bit位 范围
byte 8位 -2ˆ7~2ˆ7-1
short 2字节 -2ˆ15~2ˆ15-1, 大概3万多
int 4字节 -2ˆ31~2ˆ31-1,大概21亿
long 8字节 -2ˆ63~2ˆ63-1

整型变量:
1.直接写出的值叫直接量
2.int直接量可直接赋值给long类型,long类型的直接量需要以l/L结尾

// 1.直接写出的值叫直接量
System.out.println(9);
System.out.println(3.14);

// 2.int
// 直接写出的整数是int类型的直接量
int i = 123;
// 赋值超出范围,编译报错
// i = 3000000000;
// 运算超出范围,结果错误
i = 2147483647; // 0111...
System.out.println(i + 1); // 1000...
输出: -2147483648

// 3.byte, short, long
// int直接量可直接赋值给long类型
long l = 2;
// long类型的直接量需要以l/L结尾
l = 3000000000L;
// int直接量可直接赋值给byte,short,但不能超出其范围
byte b = 4;
// b = 128;
// b = i;
short s = 2;
// s = 32768;
// s = i;

浮点型变量:
先将小数转为科学计数法的形式,再将一串二进制的整数分为三段,分别记录小数的符号,尾数,指数。
图片说明
float:1位符号,8位指数,23位尾数 -3.4*10ˆ38 ~ 3.4*10ˆ38
double:1位符号,11位指数,52位尾数 -1.8*10ˆ308 ~ 1.8*10ˆ308
浮点数的进度不是无限的,是有误差的,如果业务需求是高精度计算,则需要用java.math.BigDecimal这个包

// 4.float,double
// 直接写出的小数是double类型的直接量
double d = 0.3;
// float类型的直接量需要以f/F结尾
float f = 0.8F;
// 浮点数不精确
System.out.println(300000.02f);
System.out.println(300000.03f);
System.out.println(3.3f + 0.1f);
输出: 300000.03; 300000.03; 3.3999999

字符型变量:
ASCII 表示256个字符
Unicode 可以表示 65535个字符,含括所有的东亚字符,java采用的是这种字符集

// 5.char
char c1 = 'A';
char c2 = 0b01000001; // 65
char c3 = '\u0041'; //unicode(16进制)

// 转义字符
char c4 = '\'';
char c5 = '\t';
char c6 = '\\';
char c7 = '\n';
// 字符串
String str = "Hello World.";
System.out.println(str);

布尔类型:
真假: true/false

2.3 基本类型的关系

图片说明
数据的相互转换:
前提是: 除了布尔类型外,其余7种均为数字
关键是:与数据类型的范围有关
方式:

  1. 自动类型 (小变大,能装下)
  2. 强制转换类型 (大变小,可能会溢出)
// 1.自动类型转换
char c = 'A';
// 自动转换,因为int的范围比char大
int i = c;
System.out.println(i);

long l = 100L;
double d = l;
System.out.println(d);

// 2.强制类型转换
int ii = 65;
char cc = (char) ii;
System.out.println(cc);

double dd = 3.14;
long ll = (long) dd;
System.out.println(ll);

// 3.运算时的自动类型转换
// 1)先将byte,short,char转为int
// 2)再将int转换为更大的类型
char ccc = 'A';
int iii = 100;
double ddd = 3.14;
System.out.println(ccc + iii + ddd);

要注意的坑⚠️⚠️⚠️

// 注意如下的坑
byte b = 8;
//(b-3)是表达式
// 如果不加(byte) 会报错: error: incompatible types: possible lossy conversion from int to byte
b = (byte) (b - 3);

// 下列情况是默认规则,不是类型转换
byte k = 5;
short m = 6;
char n = 7;

3. 运算符

3.1 运算符的分类

运算符的分类

  1. 算术运算符; 2. 关系运算符; 3. 逻辑运算符;
  2. 赋值运算符; 5. 三元运算符; 6. 字符串运算符; 7. 位运算符;

1.算术运算符

先看算术运算符

// ++a 先自增,后运算
int a = 100;
System.out.println(++a + 100);
System.out.println(a);
输出: 201, 101

// b++ 先运算,后自增
int b = 100;
System.out.println(b++ + 100);
System.out.println(b);
输出: 200, 101

关系运算符: >= == <=

3.逻辑运算符 和 短路现象

逻辑运算符: 与&&||!, 都作用布尔类型数据上
&& 或者 || 存在短路现象⚠️ 当某个逻辑值可以直接推算出结果时,后续的表达式不会被执行

int age = 32;
double salary = 40000.00;

// &&, || 存在短路现象
// salary > 50000.00 为false 因此直接返回false
System.out.println(salary > 50000.00 && ++age < 30);
System.out.println(age);
输出: false 32

// salary > 30000.00 为true 因此直接返回true
System.out.println(salary > 30000.00 || ++age < 30);
System.out.println(age);
输出: true 32

4.赋值运算符

赋值运算符: = += -= *= /= %=
y=x=1 等价于 x =1; y=x;

5.三元运算符

一定是一个赋值表达式,要有返回值

// 判断是否成年
boolean isAdault = age < 18 ? "未成年" : "成年";
// 判断阿里巴巴工资,谁高于5万,就统一砍为5万,否则维持不变
double salary = salary > 50000.00 ? 50000.00 : salary;

6.字符串运算符

字符串 + 任意类型数据 --> 字符串

// 6.字符串运算符
String s1 = "她的年龄:" + age;
System.out.println(s1);
返回: 她的年龄:32

String s2 = "她的工资:" + salary;
System.out.println(s2);
返回: 她的工资:50000.0

System.out.println("" + 100 + 200);
System.out.println(100 + 200 + "");
返回: 100200 300

7.位运算符和补码的原理

在计算机内部,负数是以补码形式存储的
期望采用加法器电路,来实现减法运算

  1. 原码
最高位存储符号(0 - 正,1 - 负),其他位存数据的绝对值。 
如:[-2]原 = 1000 0010
  1. 反码
[正数]反 = [正数]原,[负数]反 = [负数]原 除符号位外按位取反。
如:[-2]反 = 1111 1101
  1. 补码
[正数]补 = [正数]原,[负数]补 = [负数]反 + 1。 
如:[-2]补 = 1111 1110

单例推演:

1 – 1 = 1 + (-1) = 0
原码:0000 0001 + 1000 0001 = 1000 0010 = -2
反码:0000 0001 + 1111 1110 = 1111 1111 = -0
补码:0000 0001 + 1111 1111 = 0000 0000 = 0

推理:
在时钟系统中,从六点到4点可以有两种拨法: 后退两格到4,或者前进10格到4

4 = 6 + (-2) = (6 + -2 + 12) mod 12 =(6 + 10) mod 12
12是这个系统的模,-2和10同余

因此在计算机系统中,比如在计算8位数据时, 0(0000 0000) ~ 255(1111 1111)当从最大值加一后,就变成了0,因此256就是这个8位系统的模。

255 + (-3) = 255 + (256-3) mod 256 = (255 + 253) mod 256 = 508 mod 256 = 252
-3 的同余数就是 256+(-3) 其中-3 := 253(1111 1101)

在实际上,计算机里面都统一是正数,上述的补码系统是人为的一种简化计算体系,为了方便计算同余数,实际还是加法器的计算

移位运算

注意⚠️其中的,有符号右移位操作,是符号位在最高位补齐!
移位运算的最小单位是 int,如果是byte移位也会自动转换成int型
java自带的十进制数转二进制

// 如何将十进制转换为二进制
System.out.println(Integer.toBinaryString(5));

位运算的规则和特殊技巧,参考我的博客

4. 输入和流程控制

4.1 Scanner接收数据

// -->引入
import java.util.Scanner
// 创建对象
Scanner scan = new Scanner(System.in);
// 接收数据
scan.nextInt(), 
scan.nextDouble(), 
scan.nextLine(), ...
// 停止输入 
scan.close()

举个例子

// 创建Scanner对象
Scanner scan = new Scanner(System.in);

// 开始输入
System.out.println("请输入你的名字!");
String name = scan.nextLine();

System.out.println("请输入你的年龄!");
int age = scan.nextInt();

System.out.println("请输入你的学号!");
double salary = scan.nextDouble();

// 关闭Scanner
scan.close();

4.2 if语句

分支语句

if (score >= 90) {
    System.out.println("A");
} else if (score >= 80) {
    System.out.println("B");
} else if (score >= 70) {
    System.out.println("C");
} else if (score >= 60) {
    System.out.println("D");
} else {
    System.out.println("E");
}

4.3 switch 语句

switch的语法还是有点生疏

switch (表达式) { 
    case 值1: {
    ...
    break; 
    }
    case 值2: { 
    ...
    break; 
    }
    default: { 
    ...
    } 
}

注意⚠️switch里面的值是: byte, short, int, char, String, Enum. ,但不能是逻辑表达式
若不写break,只要有一个case命中,它就会顺序向下执行

switch (month) {
    case 1:
    case 2:
    case 3:
        System.out.println("第一季度");
        break;
    case 4:
    case 5:
    case 6:
        System.out.println("第二季度");
        break;
    case 7:
    case 8:
    case 9:
        System.out.println("第三季度");
        break;
    case 10:
    case 11:
    case 12:
        System.out.println("第四季度");
        break;
    default:
        System.out.println("错误的月份!");
}

4.4 if与switch对比

三元运算符 比较 if else:
三元运算符简洁但功能单一,if else里面可以放很多行,逻辑更多

if vs switch:
switch只能放表达式,只有数值匹配上才能执行分支,没有 if else 功能强大

5. 循环

  1. 循环条件
    逻辑表达式,决定了是否执行循环体。
  2. 循环体
    循环的主体,如果循环条件允许,该代码块将被重复执行。
  3. 迭代语句
    改变循环条件中的变量,使得循环条件趋近于假,并最终为假,从而导致循环结束。

5.1 while 循环

求一个十进制数有多少位

int num = 7;

// 记录整数的位数
int len = 0;

// 循环到除尽为止
while (num != 0) {
    len++;
    num /= 10;
    System.out.println("len=" + len + ", num=" + num);
}

// 不可能小于一位,考虑到num=0的情况!
len = len == 0 ? 1 : len;

5.2 do while 循环

先执行,再判断:

do{
    something;
} while(判断逻辑);

和while的区别就是,一定会先执行一次,再判断

// 循环到除尽为止, 不存在num=0的小bug
do{
    len++;
    num /= 10;
    System.out.println("len=" + len + ", num=" + num);
} while (num != 0);

5.3 for循环

for (初始化语句; 循环条件; 迭代语句){
    ...
}

例子一: 求一个数的阶乘
注意⚠️零的阶乘是1,输入的数必须是自然数

if (n < 0) {
    System.out.println("请输入一个自然数!");
} else if (n == 0) {
    System.out.println("0! = 1");
} else {
    // 记录累乘结果
    long s = 1;
    // i ∈ [1, n] 
    for (int i = 1; i <= n; i++) {
        s *= i;
    }
    System.out.println(n + "! = " + s);
}

例子二: 两个变量一起在for里面

for (int m = 0, n = 9; m < 10 && n > -1; m++, n--) {
            System.out.println("m = " + m + ", n = " + n);
    }

5.4 选择哪种循环

根据业务的需求,选择循环的方式,这里给出一个例子对比 do while 和 while循环

// 输入任意多个整数(负数代表输入结束), 求它们的平均数.
Scanner scan = new Scanner(System.in);

int n = 0; // 输入
int sum = 0; // 合计
int amount = 0; // 个数

do {
   n = scan.nextInt();
   if (n >= 0) {
       sum += n;
       amount++;
   }
} while (n >= 0);

// 或者
n = scan.nextInt();
while (n >= 0) {
    sum += n;
    amount++;
    n = scan.nextInt();
}

if (amount > 0) {
    double avg = (double) sum / amount;  
    System.out.println("平均数: " + avg);
}

scan.close();

5.5 break 和 continue 关键字

break的用法:
结束循环,强制跳出循环体。一旦遇到 break,系统将完全结束该循环,开始执循环之后的代码。

// 判断一个数,是否是质数
// 质数是,只能被自己或者1整除的数
if (n <= 1) {
    System.out.println("请输入一个大于1的正整数!");
} else {
    // 是否为质数
    boolean b = true;
    // i ∈ [2, n-1]
    for (int i = 2; i < n; i++) {
        if (n % i == 0) {
            // 可以整除, 不是质数
            b = false;
            // 得出结果, 跳出循环
            break;
        }
    }
    System.out.println(n + (b ? "是质数" : "不是质数"));
}

continue的用法:
忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环。

// 求0-n的所有的奇数的和
if (n <= 1) {
    System.out.println("请输入一个大于1的正整数!");
} else {
    // 合计值
    int sum = 0;
    // i ∈ [1, n]
    for (int i = 1; i <= n; i++) {
        if (i % 2 == 0) {
            continue;
        }
        sum += i;
    }
    System.out.println("合计值: " + sum);
}
}

5.6 嵌套循环

  1. 内层循环作为外层循环的循环体,
    每次执行外层循环,内层循环都要完整的执行一遍。

  2. 假设外层循环执行 m 次,内层循环执行 n 次,
    则程序总的循环次数为二者的乘积关系,即 m*n 次。

// 1-100的所有的质数,请打印
for (int i = 2; i <= 100; i++) {
    // 判断i是不是质数
    boolean b = true;
    // j ∈ [2, i-1]
    for (int j = 2; j < i; j++) {
        if (i % j == 0) {
            b = false;
            break;
        }
    }
// 打印判断结果
if (b) {
    System.out.print(i + "\t");
}

5.7 死循环

1. 程序的漏洞

  1. 忘记写迭代语句;
  2. 写了迭代语句,但是迭代语句向着趋近于终止的相反方向发展;
double d = 1;
while (d < 10) {
   System.out.println(d);
    // d 会越来越小,永远跳不出循环
   d *= 0.1;
}

// 可以构造的死循环
while (true) {
   System.out.println("Hello.");
}

// 可以构造的死循环
for (; ; ) {
   System.out.println("Hello.");
}

2. 刻意的营造

  1. 构造死循环,用以处理不确定次数的循环场景;
  2. 在循环体内增加判断,当条件达成时利用 break 关键字强制结束循环;
// 求平均数,用户输入数字,知道负数结束
Scanner scan = new Scanner(System.in);

int n = 0; // 输入
int sum = 0; // 合计
int amount = 0; // 个数

while (true) {
    // scan.nextInt() 保证每次可以有输入
    n = scan.nextInt();

    if (n < 0) {
        break;
    }

    sum += n;
    amount++;
}

if (amount > 0) {
    double avg = (double) sum / amount;
    System.out.println("平均数: " + avg);
}

scan.close();

5.8 变量作用域

循环语句中变量的有效范围:

  1. 在代码块内部声明的变量,只能在代码块内部使用;
  2. 在代码块前面声明的变量,可以在代码块内部使用,也可在代码块后面使用;
  3. for 循环上声明的变量,只能在循环体内部使用;
  4. do while 循环体内部声明的变量,可以在循环体内部使用,不能在循环条件中使用;
int m = 0;
while (m < 10) {
    int n = 3;
    System.out.print(m * n + "\t");
    m++;
}
// 试图打印n,但是编译器无法解析,n的生存周期已经结束了
System.out.println(n);

6. 数组

6.1 数组和其遍历

数组的静态初始化,指明其存放的内容

type[] arrayName = {element1, element2, ...}; // 声明时初始化
int[] arr = {1,2,3};
//或者
int[] arr1 = new int[] {1,2,3};

数组的动态初始化,默认存放的是 整数类型默认值为 0, 浮点类型默认值为 0.0, 字符类型默认值为 ’\u0000’, 布尔类型默认值为 false, 引用类型默认值为 null。

// 默认存放都是0
int[] arr = new int[3];

数组的长度,可以通过它的属性, arraryName.length访问得到

遍历数组的两种方式, for loop 和 foreach loop

// for loop
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]); 
}
// for each loop, 针对数组和集合
for (type variableName : array | collection) {
    System.out.println(variableName); 
}

6.2 数据的工具类

数组工具类

Arrays.copyOf是属于浅拷贝,若原数组内存放的是基本类型,则是值传递,若内部存放的是对象,则是引用传递,修改一个对象会引起原数组对象的修改。

6.3 内存中的数组

在java中,数组是引用类型,是指向数组连续内存的首地址

int[] arr1 = {10, 20, 30, 40, 50};
// 引用类型赋值
int[] arr2 = arr1;

图片说明

6.4 多维数组

初始化:

// 静态初始化
int[][] arr1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
// 动态初始化
int[][] arr2 = new int[5][2];

打印多纬数组:

Arrays.deepToString(arr);

本质是一维数组,可以不是绝对的矩形:

// 本质是一维
int[][] arr3 = new int[3][];
arr3[0] = new int[]{1, 2};
arr3[1] = new int[]{3, 4, 5};
arr3[2] = new int[]{6, 7, 8, 9};

6.5 数组反转

不调用类方法,去底层实现数组反转
对称的交换可以减少一半的常数时间,相比于反序遍历

// 倒转数组
for (int i = 0; i < arr.length / 2; i++) {
    // arr[0] ~ arr[length-1-0]
    // arr[1] ~ arr[length-1-1]
    // arr[2] ~ arr[length-1-2]
    // arr[i] ~ arr[length-1-i]
    // tips: t = a; a = b; b = t;
    int temp = arr[i];
    arr[i] = arr[arr.length - 1 - i];
    arr[arr.length - 1 - i] = temp;
}

6.6 数组求平均值

需求:

  1. 输入任意多个整数,直到输入负数为止;
  2. 计算这些整数的平均值,并打印出结果;
// 初始数组长度设为5
int[] arr = new int[5];
// 当前输入数字下标
int index = 0;
// 循环输入,直到遇到-1停止
int sum = 0;
int len = 0;
double average = 0;
Scanner sc = new Scanner(System.in);
while (true){
    int cur = sc.nextInt();
    if(cur == -1){
        sc.close();
        average = len==0 ? 0 : (double)sum/len;
        break;
    }
    arr[index++] = cur;
    sum += cur;
    len++;
    // 数组扩容二倍
    if(len == 5){
        arr = Arrays.copyOf(arr, arr.length * 2);
    }
}

6.7 数组冒泡排序

最基本的排序思想:

  1. 每一轮比较出一个最大(小)值,将其移动到数组最右(左)端;
  2. 对于长度为 N 的数组,需历经 N-1 轮才能完成所有的比较。
// 轮次: length -1
for (int i = 0; i < arr.length - 1; i++) {
    // 比较的次数
    // 0: 0 ~ length - 1
    // 1: 0 ~ length - 1 - 1
    // 2: 0 ~ length - 1 - 2
    // i: 0 ~ length - 1 - i
    // 循环的边界
    // j = arr.length - 1 - i - 1
    // j + 1 = arr.length - 1 - i
    for (int j = 0; j < arr.length - 1 - i; j++) {
        // 从大到小: 将小的数从左向右转移.
        if (arr[j] < arr[j + 1]) {
            int temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
}

7. 方法

7.1 参数

函数的参数分为,基本类型参数 和 引用类型参数.
在main函数中,程序执行是由栈帧控制的,swapNum传入的是基本类型,在各自的栈帧内会为a和b分配内存空间,一个函数的操作不会影响另一个。
在函数swapArray中传入的是引用类型的参数,实际传入的是引用,指向相同的内存空间,因此会相互影响修改。

public static void main(String[] args) {
    int a = 1;
    int b = 2;
    swapNum(a, b);
    System.out.println("a = " + a + ", b = " + b);

    int[] arr = {1, 2, 3, 4, 5};
    swapArray(arr);
    System.out.println("arr = " + Arrays.toString(arr));
}

// 交换两个数
public static void swapNum(int m, int n) {
    int t = m;
    m = n;
    n = t;
    System.out.println("m = " + m + ", n = " + n);
}

// 交换数组首尾的两个数
public static void swapArray(int[] array) {
    if (array == null || array.length < 2) {
        return;
    }

    int t = array[0];
    array[0] = array[array.length - 1];
    array[array.length - 1] = t;
}

7.2 可变参数

什么是可变参数

  1. 在定义方法时,可以声明数量不确定的参数,这样的参数叫可变的参数;
  2. 一个方法最多声明一个可变参数,并且该参数必须位于参数列表的末尾;
  3. 可变参数的本质是一个数组,调用时可以分开传入多个值,也可以直接传入一个数组。

◼ 如何声明可变参数
修饰符 返回值类型 方法名(类型 参数1, 类型 参数2, ..., 类型... 参数N) { }

public static void main(String[] args) {
    System.out.println(sum());
    System.out.println(sum(1));
    System.out.println(sum(1, 2));
    System.out.println(sum(1, 2, 3));
    System.out.println(sum(new int[]{1, 2, 3, 4, 5}));
}

public static int sum(int... nums) {
    int s = 0;

    for (int num : nums) {
        s += num;
    }

    return s;
}

7.3 方法重载

◼ 什么是方法重载
在同一个类里,定义多个名称相同、参数列表不同的方法,叫做方法重载。
若只是参数名字不一样不是方法重载,参数名字和类型一样但返回值类型不一样不是方法重载

◼ 方法重载的作用
对于调用者而言,多个重载的方法就像是一个方法,便于记忆、便于调用。

总结:
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
参考资料

如: System.out.println(), Arrays.toString(), Math.abs() 都是方法重载

// java中Arrays.toString()的源码,针对不同类型的参数,重载了toString这个方法
public static String toString(int[] a) {
    if (a == null)
        return "null";
    int iMax = a.length - 1;
    if (iMax == -1)
        return "[]";

    StringBuilder b = new StringBuilder();
    b.append('[');
    for (int i = 0; ; i++) {
        b.append(a[i]);
        if (i == iMax)
            return b.append(']').toString();
        b.append(", ");
    }
}

public static String toString(float[] a) {
    if (a == null)
        return "null";

    int iMax = a.length - 1;
    if (iMax == -1)
        return "[]";

    StringBuilder b = new StringBuilder();
    b.append('[');
    for (int i = 0; ; i++) {
        b.append(a[i]);
        if (i == iMax)
            return b.append(']').toString();
        b.append(", ");
    }
}

7.4 调试程序

idea里面设置断点,调试程序,常用的快捷方式
执行下一步,调入方法内部,执行到下个断点