JAVA学习之String学习

news2024/11/19 3:47:12

1.底层是用什么实现的?

JDK8用的char数组,JDK9开始使用byte数组,而且都是final型,所以不同字符串(值)的地址必然不同。
在这里插入图片描述

在这里插入图片描述
char和byte的区别:char是2个字节表示,而byte是一个字节。
JDK17中,String使用2种编码方式 LATIN1和UTF-16.
但是转byte时就是用UTF-8进行保存了
LATIN1是单字节
UTF-16是双字节
所以当存汉字的时候,String的数据扩容就不同了,这样看,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2.String的构造方法

	//     private final byte[] value;
	//1. 空串
    public String() {
        this.value = "".value;
        this.coder = "".coder;
    }
    // 2.字符串赋值
 public String(String original) {
        this.value = original.value;
        this.coder = original.coder;
        this.hash = original.hash;
    }
 	// 3.   char 转String  char数组 开头 长度 编码方式(默认8)
 public String(char value[]) {
        this(value, 0, value.length, null);
    }
    
 public String(char value[], int offset, int count) {
        this(value, offset, count, rangeCheck(value, offset, count));
    }
    
        String(byte[] value, byte coder) {
        this.value = value;
        this.coder = coder;
    }
    // 4.第四个VOID 没有用,作为执行添头吧
    String(char[] value, int off, int len, Void sig) {
        if (len == 0) {
            this.value = "".value;
            this.coder = "".coder;
            return;
        }
        if (COMPACT_STRINGS) {
            byte[] val = StringUTF16.compress(value, off, len);
            if (val != null) {
                this.value = val;
                this.coder = LATIN1;
                return;
            }
        }
        this.coder = UTF16;
        this.value = StringUTF16.toBytes(value, off, len);
    }
    // int只有这一种,数组 开始位置 长度
   public String(int[] codePoints, int offset, int count) {
        checkBoundsOffCount(offset, count, codePoints.length);
        if (count == 0) {
            this.value = "".value;
            this.coder = "".coder;
            return;
        }
        if (COMPACT_STRINGS) {
            byte[] val = StringLatin1.toBytes(codePoints, offset, count);
            if (val != null) {
                this.coder = LATIN1;
                this.value = val;
                return;
            }
        }
        this.coder = UTF16;
        this.value = StringUTF16.toBytes(codePoints, offset, count);
    }
// 4个参数  byte数组 截取开始位置  截取长度  编码方式  
// 代码长是因为要转换编码方式
 public String(byte[] bytes, int offset, int length, Charset charset) {
        Objects.requireNonNull(charset);
        checkBoundsOffCount(offset, length, bytes.length);
        if (length == 0) {
            this.value = "".value;
            this.coder = "".coder;
        } else if (charset == UTF_8.INSTANCE) {
            if (COMPACT_STRINGS && !StringCoding.hasNegatives(bytes, offset, length)) {
                this.value = Arrays.copyOfRange(bytes, offset, offset + length);
                this.coder = LATIN1;
            } else {
                int sl = offset + length;
                int dp = 0;
                byte[] dst = null;
                if (COMPACT_STRINGS) {
                    dst = new byte[length];
                    while (offset < sl) {
                        int b1 = bytes[offset];
                        if (b1 >= 0) {
                            dst[dp++] = (byte)b1;
                            offset++;
                            continue;
                        }
                        if ((b1 == (byte)0xc2 || b1 == (byte)0xc3) &&
                                offset + 1 < sl) {
                            int b2 = bytes[offset + 1];
                            if (!isNotContinuation(b2)) {
                                dst[dp++] = (byte)decode2(b1, b2);
                                offset += 2;
                                continue;
                            }
                        }
                        // anything not a latin1, including the repl
                        // we have to go with the utf16
                        break;
                    }
                    if (offset == sl) {
                        if (dp != dst.length) {
                            dst = Arrays.copyOf(dst, dp);
                        }
                        this.value = dst;
                        this.coder = LATIN1;
                        return;
                    }
                }
                if (dp == 0 || dst == null) {
                    dst = new byte[length << 1];
                } else {
                    byte[] buf = new byte[length << 1];
                    StringLatin1.inflate(dst, 0, buf, 0, dp);
                    dst = buf;
                }
                dp = decodeUTF8_UTF16(bytes, offset, sl, dst, dp, true);
                if (dp != length) {
                    dst = Arrays.copyOf(dst, dp << 1);
                }
                this.value = dst;
                this.coder = UTF16;
            }
        } else if (charset == ISO_8859_1.INSTANCE) {
            if (COMPACT_STRINGS) {
                this.value = Arrays.copyOfRange(bytes, offset, offset + length);
                this.coder = LATIN1;
            } else {
                this.value = StringLatin1.inflate(bytes, offset, length);
                this.coder = UTF16;
            }
        } else if (charset == US_ASCII.INSTANCE) {
            if (COMPACT_STRINGS && !StringCoding.hasNegatives(bytes, offset, length)) {
                this.value = Arrays.copyOfRange(bytes, offset, offset + length);
                this.coder = LATIN1;
            } else {
                byte[] dst = new byte[length << 1];
                int dp = 0;
                while (dp < length) {
                    int b = bytes[offset++];
                    StringUTF16.putChar(dst, dp++, (b >= 0) ? (char) b : REPL);
                }
                this.value = dst;
                this.coder = UTF16;
            }
        } else {
            // (1)We never cache the "external" cs, the only benefit of creating
            // an additional StringDe/Encoder object to wrap it is to share the
            // de/encode() method. These SD/E objects are short-lived, the young-gen
            // gc should be able to take care of them well. But the best approach
            // is still not to generate them if not really necessary.
            // (2)The defensive copy of the input byte/char[] has a big performance
            // impact, as well as the outgoing result byte/char[]. Need to do the
            // optimization check of (sm==null && classLoader0==null) for both.
            CharsetDecoder cd = charset.newDecoder();
            // ArrayDecoder fastpaths
            if (cd instanceof ArrayDecoder ad) {
                // ascii
                if (ad.isASCIICompatible() && !StringCoding.hasNegatives(bytes, offset, length)) {
                    if (COMPACT_STRINGS) {
                        this.value = Arrays.copyOfRange(bytes, offset, offset + length);
                        this.coder = LATIN1;
                        return;
                    }
                    this.value = StringLatin1.inflate(bytes, offset, length);
                    this.coder = UTF16;
                    return;
                }

                // fastpath for always Latin1 decodable single byte
                if (COMPACT_STRINGS && ad.isLatin1Decodable()) {
                    byte[] dst = new byte[length];
                    ad.decodeToLatin1(bytes, offset, length, dst);
                    this.value = dst;
                    this.coder = LATIN1;
                    return;
                }

                int en = scale(length, cd.maxCharsPerByte());
                cd.onMalformedInput(CodingErrorAction.REPLACE)
                        .onUnmappableCharacter(CodingErrorAction.REPLACE);
                char[] ca = new char[en];
                int clen = ad.decode(bytes, offset, length, ca);
                if (COMPACT_STRINGS) {
                    byte[] bs = StringUTF16.compress(ca, 0, clen);
                    if (bs != null) {
                        value = bs;
                        coder = LATIN1;
                        return;
                    }
                }
                coder = UTF16;
                value = StringUTF16.toBytes(ca, 0, clen);
                return;
            }

            // decode using CharsetDecoder
            int en = scale(length, cd.maxCharsPerByte());
            cd.onMalformedInput(CodingErrorAction.REPLACE)
                    .onUnmappableCharacter(CodingErrorAction.REPLACE);
            char[] ca = new char[en];
            if (charset.getClass().getClassLoader0() != null &&
                    System.getSecurityManager() != null) {
                bytes = Arrays.copyOfRange(bytes, offset, offset + length);
                offset = 0;
            }

            int caLen = decodeWithDecoder(cd, ca, bytes, offset, length);
            if (COMPACT_STRINGS) {
                byte[] bs = StringUTF16.compress(ca, 0, caLen);
                if (bs != null) {
                    value = bs;
                    coder = LATIN1;
                    return;
                }
            }
            coder = UTF16;
            value = StringUTF16.toBytes(ca, 0, caLen);
        }
    }

    /*
     * Throws iae, instead of replacing, if malformed or unmappable.
     */
String(char[] value, int off, int len, Void sig) {
    if (len == 0) {
        this.value = "".value;
        this.coder = "".coder;
        return;
    }
    if (COMPACT_STRINGS) {
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {
            this.value = val;
            this.coder = LATIN1;
            return;
        }
    }
    this.coder = UTF16;
    this.value = StringUTF16.toBytes(value, off, len);
}

小总结一下
这里暂时不考虑int型
参数 :1.直接数组(char byte)2.数组+2个参数(第一个参数是截取的开始位置,第二个位置是截取长度)3。带编码方式的(只有byte可以) 数组+编码方式 或者编码方式放在最后
注意,编码方式既可以是String型,也可以是CharSet型,因为lookupCharset(charsetName) 可以转换编码方式

2.获取想要的编码方式的byte数组

public byte[] getBytes(Charset charset) {
    if (charset == null) throw new NullPointerException();
    return encode(charset, coder(), value);
 }
	// CharSet格式 StandardCharSet.
   s2.getBytes(StandardCharsets.UTF_16); 

在这里插入图片描述

3.关于String初始化的思考

从String的初始化看,都是Value的直接赋值,那么问题来了。赋值的前提是Value存在,也就是说,我之前的玩意一定在,所以,他也要占空间
例如:String(“asda”) String(byte a[])
这两个,构造方法调用的前提都是,字符串String ="asda"存在,byte a[] 存在。
所以如果是new String 最起码会创建一个对象,也可能会创建两个。

4.方法学习

1.获取某个位置的字符

// index是要获取的字符下标
public char charAt(int index) 

2.普通比较

2.比较

// 转成相同的字符编码,然后进行比较 先比长度,再逐个比较就完事了

public int compareTo(String anotherString) {
        byte v1[] = value;
        byte v2[] = anotherString.value;
        byte coder = coder();
        if (coder == anotherString.coder()) {
            return coder == LATIN1 ? StringLatin1.compareTo(v1, v2)
                                   : StringUTF16.compareTo(v1, v2);
        }
        return coder == LATIN1 ? StringLatin1.compareToUTF16(v1, v2)
                               : StringUTF16.compareToLatin1(v1, v2);
     }

3.无视大小写比较

还是先判断编码格式,按照编码格式先转码然后再去比较,不过这个比较之前,都先转化成了大写然后比较的

public int compareToIgnoreCase(String str) {
    return CASE_INSENSITIVE_ORDER.compare(this, str);
}
 public int compare(String s1, String s2) {
            byte v1[] = s1.value;
            byte v2[] = s2.value;
            byte coder = s1.coder();
            if (coder == s2.coder()) {
                return coder == LATIN1 ? StringLatin1.compareToCI(v1, v2)
                                       : StringUTF16.compareToCI(v1, v2);
            }
            return coder == LATIN1 ? StringLatin1.compareToCI_UTF16(v1, v2)
                                   : StringUTF16.compareToCI_Latin1(v1, v2);
        }
public static int compareToCI(byte[] value, byte[] other) {
    int len1 = value.length;
    int len2 = other.length;
    int lim = Math.min(len1, len2);
    for (int k = 0; k < lim; k++) {
        if (value[k] != other[k]) {
            char c1 = (char) CharacterDataLatin1.instance.toUpperCase(getChar(value, k));
            char c2 = (char) CharacterDataLatin1.instance.toUpperCase(getChar(other, k));
            if (c1 != c2) {
                c1 = Character.toLowerCase(c1);
                c2 = Character.toLowerCase(c2);
                if (c1 != c2) {
                    return c1 - c2;
                }
            }
        }
    }
    return len1 - len2;
}

4.concat 拼接

垃圾方法 因为我门可以直接使用+

5.contains查询某个字符或者字符串是否存在于这个字符串中

好笑的是,里面用的还是indexof方法
注意这里的charSequence 字符串和字符都继承了这个接口,所以s可以是字符和字符串

public boolean contains(CharSequence s) {
    return indexOf(s.toString()) >= 0;
}

6.indexof

按照相应的编码方式去寻找,因为不同编码方式,长度不同,位置不同

 public int indexOf(int ch) {
        return indexOf(ch, 0);
    }
public int indexOf(int ch, int fromIndex) {
    return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex)
                      : StringUTF16.indexOf(value, ch, fromIndex);
}

7.判断是不是以某个字符串为结尾(开始)

思路:
1.按照编码方式进行对比,不符合要求直接pass
2.(转码)比较
//


  public boolean endsWith(String suffix) {
        return startsWith(suffix, length() - suffix.length());
    }
    // 2个参数 一个是字符串,一个是开始位置
     public boolean startsWith(String prefix, int toffset) {
        // Note: toffset might be near -1>>>1.
        if (toffset < 0 || toffset > length() - prefix.length()) {
            return false;
        }
        byte ta[] = value;
        byte pa[] = prefix.value;
        int po = 0;
        int pc = pa.length;
        byte coder = coder();
        // 编码方式相同
        // 编码方式不同的话, 必须传来的参数是拉丁的
        //不同的话,如果传来的是UTF16,则本字符串就是拉丁,拉丁无汉字,直接不用比较了
        if (coder == prefix.coder()) { 
         // 看编码格式相等不相等,相等就找到相应的位置,因为汉字UTF-16的位置需要双倍一下  如果是汉字 字母格式 就进行2位16进制的比较 2个16进制就是一个字节(8位二进制)
            int to = (coder == LATIN1) ? toffset : toffset << 1;                                                                                                                                                          
            while (po < pc) {
            //走到相应位置直接开始比较
                if (ta[to++] != pa[po++]) {
                    return false;
                }
            }
        } else {
            if (coder == LATIN1) {  // && pcoder == UTF16
                return false;
            }
            // coder == UTF16 && pcoder == LATIN1)
            while (po < pc) {
                if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
                    return false;
               }
            }
        }
        return true;
    }

8.split 分割

处理符号 将每一段String都放到list中,最后返回String数组
如果没有分割符,就返回该字符串。

public String[] split(String regex) {
    return split(regex, 0);
}
    public String[] split(String regex, int limit) {
    /* fastpath if the regex is a
     * (1) one-char String and this character is not one of the
     *     RegEx's meta characters ".$|()[{^?*+\\", or
     * (2) two-char String and the first char is the backslash and
     *     the second is not the ascii digit or ascii letter.
     */
    char ch = 0;
    if (((regex.length() == 1 &&
         ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
         (regex.length() == 2 &&
          regex.charAt(0) == '\\' &&
          (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
          ((ch-'a')|('z'-ch)) < 0 &&
          ((ch-'A')|('Z'-ch)) < 0)) &&
        (ch < Character.MIN_HIGH_SURROGATE ||
         ch > Character.MAX_LOW_SURROGATE))
    {
        int off = 0;
        int next = 0;
        boolean limited = limit > 0;
        ArrayList<String> list = new ArrayList<>();
        while ((next = indexOf(ch, off)) != -1) {
            if (!limited || list.size() < limit - 1) {
                list.add(substring(off, next));
                off = next + 1;
            } else {    // last one
                //assert (list.size() == limit - 1);
                int last = length();
                list.add(substring(off, last));
                off = last;
                break;
            }
        }
        // If no match was found, return this
        if (off == 0)
            return new String[]{this};

        // Add remaining segment
        if (!limited || list.size() < limit)
            list.add(substring(off, length()));

        // Construct result
        int resultSize = list.size();
        if (limit == 0) {
            while (resultSize > 0 && list.get(resultSize - 1).isEmpty()) {
                resultSize--;
            }
        }
        String[] result = new String[resultSize];
        return list.subList(0, resultSize).toArray(result);
    }
    return Pattern.compile(regex).split(this, limit);
}

在这里插入图片描述

8. trip清除边缘空格

strip和stripIndent方法一样,都是去除两边空格
stripLeading 去除左边空格
stripTrailing 去除右边空格
在这里插入图片描述

9.截取字符串substring

取前不取后,因为方法中算的是长度(后-前)

public String substring(int beginIndex) {
    return substring(beginIndex, length());
}
    public String substring(int beginIndex, int endIndex) {
    int length = length();
    checkBoundsBeginEnd(beginIndex, endIndex, length);
    if (beginIndex == 0 && endIndex == length) {
        return this;
    }
    int subLen = endIndex - beginIndex;
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                      : StringUTF16.newString(value, beginIndex, subLen);
}

在这里插入图片描述

10.大小转换

    public String toLowerCase() {
        return toLowerCase(Locale.getDefault());
    }
 public String toUpperCase() {
        return toUpperCase(Locale.getDefault());
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/699787.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3解决:[Vue warn]: Failed to resolve component: el-table(或el-button) 的三种解决方案

1、问题描述&#xff1a; 其一、报错为&#xff1a; [Vue warn]: Failed to resolve component: el-table If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement. at <App> 或者&#xff1a; …

网络安全(黑客)自学

建议一&#xff1a;黑客七个等级 黑客&#xff0c;对很多人来说充满诱惑力。很多人可以发现这门领域如同任何一门领域&#xff0c;越深入越敬畏&#xff0c;知识如海洋&#xff0c;黑客也存在一些等级&#xff0c;参考知道创宇 CEO ic&#xff08;世界顶级黑客团队 0x557 成员&…

kubectl-ai:K8S资源清单的GPT助手

琦彦&#xff0c;在 **云原生百宝箱 **公众号等你&#xff0c;与你一起探讨应用迁移&#xff0c;GitOps&#xff0c;二次开发&#xff0c;解决方案&#xff0c;CNCF生态&#xff0c;及生活况味。 kubectl-ai 项目是一个kubectl使用 OpenAI GPT 生成和应用 Kubernetes 清单的插件…

【APP自动化测试必知必会】Appium之微信小程序自动化测试

本节大纲 H5 与小程序介绍 混合 App 元素定位环境部署 混合 App 元素操作 Airtest 测试 App 01.H5与小程序介绍 H5概述 H5 是指第 5 代 HTML &#xff0c;也指用 H5 语言制作的一切数字产品。 所谓 HTML 是“超文本标记语言”的英文缩写。我们上网所看到网页&#xf…

Oculus创始人谈Vision Pro:苹果在硬件设计、营销都做对了选择

早在Vision Pro正式发布之前&#xff0c;Oculus创始人Palmer Luckey就已经体验过早期版本&#xff0c;并给出了极高的评价。Luckey指出&#xff0c;苹果在XR头显上的策略是明智的&#xff0c;先打造出每个人预期中的头显&#xff0c;然后再去考虑如何让大家买得起。 Vision Pro…

远程控制电脑软件VNC安装使用教程:Windows系统

什么是VNC&#xff1f; VNC (Virtual Network Console)&#xff0c;即虚拟网络控制台&#xff0c;它是一款基于 UNIX 和 Linux 操作系统的优秀远程控制工具软件&#xff0c;由著名的 AT&T 的欧洲研究实验室开发&#xff0c;远程控制能力强大&#xff0c;高效实用&#xff…

【python】python编程基础

基础工具包 python 原生数据结构元组 Tuple列表 list集合 set字典 dictionary NumPy 数据结构数组 Ndarray矩阵 Matrix Pandas 数据结构序列 Series &#xff08;一维&#xff09;数据框 DataFrame &#xff08;二维&#xff09; Matplotlib 数据可视化绘制饼图绘制折线图绘制直…

《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

一文读懂高分文章必备分析-GSEA

Gene Set Enrichment Analysis 或称 GSEA&#xff0c;是一种常用于转录组基因表达分析的数据挖掘技术&#xff0c;已经在《nature》、《Cell》、《ISME》、《Molecular Cell》、《Bioactive Materials》等高分杂志中发表多篇文章&#xff0c;涉及转录组及多组学内容。 凌恩生物…

yxcms存储型XSS至getshell 漏洞复现

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 环境部署02 漏洞配置03 利用方式04 修复方案 01 环境部署 &#xff08;1&#xff09;yxcms yxcms 基于 PHPMySQL 开发&#xff0c;这是一个采用轻量级 MVC 设计模式的网站管理系统。轻量级 MVC 设…

13-Cookie、Session、Token

目录 1.前置知识——HTTP协议 1.1.HTTP 的主要特点有以下 5 个&#xff1a; 1.2.HTTP 组成 1.3.为什么会有Cookie、Session、Token&#xff1f; 2.Cookie 3.Session PS&#xff1a;Cookie 和 Session 的联系与区别 4.Token 4.1.token的组成 4.2.token是如何生成的&am…

【广州华锐互动】VR航天航空体验展厅提供沉浸式的展示效果

VR航天航空体验展厅是一种基于虚拟现实技术的在线展览形式&#xff0c;它通过模拟真实的太空环境&#xff0c;为用户提供了一种身临其境的参观体验。与传统的线上展览相比&#xff0c;VR航天航空体验展厅具有以下几个特色&#xff1a; 1.沉浸式体验&#xff1a;VR航天航空体验…

史上最详细的webrtc-streamer访问摄像机视频流教程

目录 前言 一、webrtc-streamer的API 二、webrtc-streamer的启动命令介绍 1.原文 2.译文 三、webrtc-streamer的安装部署 1.下载地址 https://github.com/mpromonet/webrtc-streamer/releases 2.windows版本部署 3.Linux版本部署 四、springboot整合webrtc-streamer …

技术不断变革,亚马逊云科技中国峰会引领企业重塑业务

过去十年&#xff0c;数字化转型的浪潮携带着机遇和挑战席卷而来&#xff0c;几乎每个企业都在做数字化转型&#xff0c;开始向大数据、人工智能等新技术寻求生产力的突破。但随着数字化转型深入&#xff0c;很多企业开始感受到数字化投入的成本压力&#xff0c;加之新技术正带…

Alibi:Attention With Linear Biases Enables Input Length Extrapolation

Alibi:Attention With Linear Biases Enables Input Length Extrapolation IntroductionMethodResult参考 Introduction 假设一个模型在512token上做训练&#xff0c;在推理的时候&#xff0c;模型在更长的序列上表现叫做模型的外推性。作者表明以前的位置编码如Sin、Rotary、…

JS 数据变化监听函数封装

文章目录 监听函数使用用例重复添加函数&#xff0c;只有最后一个监听函数有效 监听函数 /*** 监听函数* param {对象} vm * param {键值} key * param {触发函数} action */ function WatchValueChange(vm, key, action) {var val vm[key]Object.defineProperty(vm, key, {e…

阿里内部流传出来的《1000 道互联网大厂 Java 工程师面试题》来袭,面试必刷,跳槽大厂神器

眼看着"金九银十"也快到来了&#xff0c;很多小伙伴都蠢蠢欲动想要刚给自己涨一波薪资&#xff1b;面试作为涨薪最直接最有效的方式&#xff0c;我们需要花费巨大的精力和时间来准备。除了自身的技术积累之外&#xff0c;掌握一定的面试技巧和熟悉最常见的面试题&…

掌握imgproc组件:opencv-图像轮廓与图像分割修复

图像轮廓与图像分割修复 1.查找并绘制轮廓1.1 寻找轮廓&#xff1a;findContours()函数1.2 绘制轮廓&#xff1a;drawContours()函数1.3 案例程序&#xff1a;轮廓查找 2. 寻找物体的凸包2.1 凸包2.2 寻找凸包&#xff1a;convexHull()函数2.3 案例&#xff1a;寻找和绘制物体的…

【前端|CSS系列第1篇】CSS零基础入门之CSS的基本概念和选择器

欢迎来到CSS零基础入门系列的第一篇博客&#xff01;在这个系列中&#xff0c;我们将一起学习CSS&#xff08;层叠样式表&#xff09;的基础知识&#xff0c;探索如何为网页添加样式和布局。本篇博客将重点介绍CSS的基本概念和选择器&#xff0c;帮助你理解CSS的核心概念。 1.…

MAYA活塞(使用骨骼)

复制骨骼 P父子关系 创建组