Dictionaries in ABAP. How?

Well, how about the following solution?

REPORT ZZZ.

TYPES: BEGIN OF t_phone_number,
  name TYPE char40,
  number TYPE char40,
  END OF t_phone_number.

DATA: gt_phone_number TYPE HASHED TABLE OF t_phone_number WITH UNIQUE KEY name.

START-OF-SELECTION.
  gt_phone_number = VALUE #(
    ( name = 'hans' number = '++498912345' )
    ( name = 'peter' number = '++492169837' )
    ( name = 'alice' number = '++6720915' )
  ).

* access
  WRITE / gt_phone_number[ name = 'hans' ]-number.

* add
  gt_phone_number = VALUE #( BASE gt_phone_number ( name = 'bernd' number = '++3912345' ) ).

* update
  MODIFY TABLE gt_phone_number FROM VALUE #( name = 'bernd' number = '++123456' ).

  IF line_exists( gt_phone_number[ name = 'alice' ] ).
    WRITE / 'Yes, Alice is known.'.
  ENDIF.

* all entries
  LOOP AT gt_phone_number ASSIGNING FIELD-SYMBOL(<g_phone_number>).
    WRITE: /, <g_phone_number>-name, <g_phone_number>-number.
  ENDLOOP.

@Jagger's answer is great, but @guettli asked for shorter syntax. So just for completeness, there is of course always the possibility to wrap this in a class:

CLASS dictionary DEFINITION.

  PUBLIC SECTION.

    TYPES:
      BEGIN OF row_type,
        key  TYPE string,
        data TYPE string,
      END OF row_type.

    TYPES hashed_map_type TYPE HASHED TABLE OF row_type WITH UNIQUE KEY key.

    METHODS put
      IMPORTING
        key  TYPE string
        data TYPE string.

    METHODS get
      IMPORTING
        key           TYPE string
      RETURNING
        VALUE(result) TYPE string.

    METHODS get_all
      RETURNING
        VALUE(result) TYPE hashed_map_type.

    METHODS contains
      IMPORTING
        key           TYPE string
      RETURNING
        VALUE(result) TYPE abap_bool.

  PRIVATE SECTION.
    DATA map TYPE hashed_map_type.

ENDCLASS.

CLASS dictionary IMPLEMENTATION.

  METHOD put.
    READ TABLE map REFERENCE INTO DATA(row) WITH TABLE KEY key = key.
    IF sy-subrc = 0.
      row->*-data = data.
    ELSE.
      INSERT VALUE #( key  = key
                      data = data )
        INTO TABLE map.
    ENDIF.
  ENDMETHOD.

  METHOD get.
    result = map[ key = key ]-data.
  ENDMETHOD.

  METHOD get_all.
    INSERT LINES OF map INTO TABLE result.
  ENDMETHOD.

  METHOD contains.
    result = xsdbool( line_exists( map[ key = key ] ) ).
  ENDMETHOD.

ENDCLASS.

Leading to:

DATA(phone_numbers) = NEW dictionary( ).

phone_numbers->put( key = 'hans' data = '++498912345' ).
phone_numbers->put( key = 'peter' data = '++492169837' ).
phone_numbers->put( key = 'alice' data = '++6720915' ).

" access
WRITE phone_numbers->get( 'hans' ).

" add
phone_numbers->put( key = 'bernd' data = '++3912345' ).

" update
phone_numbers->put( key = 'bernd' data = '++123456' ).

IF phone_numbers->contains( 'alice' ).
  WRITE 'Yes, alice is known'.
ENDIF.

" all entries
LOOP AT phone_numbers->get_all( ) INTO DATA(row).
  WRITE: / row-key, row-data.
ENDLOOP.

People rarely do this in ABAP because internal tables are so versatile and powerful. From my personal point of view, I'd like to see people build more custom data structures. Implementation details like HASHED or SORTED, see discussion in @Jagger's answer, are hidden away in a natural way when doing this.